mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-06 17:46:57 +02:00
Merge branch 'master' into oci-auth-instance-principal
This commit is contained in:
commit
ce2ccebb2e
15
.dockerignore
Normal file
15
.dockerignore
Normal file
@ -0,0 +1,15 @@
|
||||
# Git Related Items
|
||||
.git
|
||||
.github
|
||||
.gitignore
|
||||
|
||||
# CI Related Items
|
||||
.travis.yml
|
||||
cloudbuild.yaml
|
||||
.golangci.yml
|
||||
.zappr.yaml
|
||||
|
||||
# Other
|
||||
docs
|
||||
OWNERS
|
||||
vendor
|
3
.github/labeler.yml
vendored
3
.github/labeler.yml
vendored
@ -11,6 +11,9 @@ provider/aws: provider/aws*
|
||||
# Add 'provider/azure' in file which starts with azure
|
||||
provider/azure: provider/azure*
|
||||
|
||||
# Add 'provider/bluecat' in file which starts with bluecat
|
||||
provider/bluecat: provider/bluecat*
|
||||
|
||||
# Add 'provider/cloudflare' in file which starts with cloudflare
|
||||
provider/cloudflare: provider/cloudflare*
|
||||
|
||||
|
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@ -16,7 +16,7 @@ jobs:
|
||||
- name: Set up Go 1.x
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ^1.15
|
||||
go-version: ^1.16
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -48,3 +48,6 @@ external-dns
|
||||
vendor/
|
||||
|
||||
profile.cov
|
||||
|
||||
# github codespaces
|
||||
.venv/
|
571
CHANGELOG.md
571
CHANGELOG.md
@ -1,571 +0,0 @@
|
||||
## Unreleased
|
||||
|
||||
- Add quick start section to contributing docs (#1766) @seanmalloy
|
||||
- Enhance pull request template @seanmalloy
|
||||
- Improve errors context for AWS provider
|
||||
- Scaleway Provider (#1643) @Sh4d1
|
||||
- Enable azure_private_dns to work with non "AzurePublicCloud" clouds (#1578) @daddonpa
|
||||
- Fix typos in documentation @ddymko
|
||||
- Add Cloudflare documentation on use of `--zone-id-filter` (#1751) @loozhengyuan
|
||||
- Fix: alibaba cloud keeping create record (#1682) @LXM
|
||||
- Update all container registry references to use k8s.gcr.io @seanmalloy
|
||||
- Provide available prometheus metrics in documentation @vinny-sabatini
|
||||
- Fix index out of range when hostname has no dots (#1756) @chemasan
|
||||
- Fixes test coverage with coveralls (#1755) @jgrumboe
|
||||
- Add tutorial for GKE with workload identity (#1765) @ddgenome
|
||||
- Fix NodePort with externaltrafficpolicy targets duplication @codearky
|
||||
- Update contributing section in README (#1760) @seanmalloy
|
||||
- Option to cache AWS zones list @bpineau
|
||||
- Oracle OCI provider: add support for instance principal authentication (#1700) @ericrrath
|
||||
- Refactor, enhance and test Akamai provider and documentation (#1846) @edglynes
|
||||
- Fix: only use absolute CNAMEs in Scaleway provider (#1859) @Sh4d1
|
||||
|
||||
## v0.7.3 - 2020-08-05
|
||||
|
||||
- Fix: add serviceaccount name in kustomize deployment (#1689) @jmthvt
|
||||
- Updates Oracle OCI SDK to latest (#1687) @ericrrath
|
||||
- UltraDNS Provider (#1635) @kbhandari
|
||||
- Update apiVersions in docs (#1690) @ddgenome
|
||||
- use the github actions build status badge (#1702) @tariq1890
|
||||
- Upgrade Oracle OCI SDK (#1688) @ericrrath
|
||||
- update dependencies and minor dep tree cleanup (#1692) @tariq1890
|
||||
- Update link for linode cloud manager (#1661) @phillc
|
||||
- Remove occurrences of "master" from the project (#1636) @Raffo
|
||||
- Create pull_request_template (#1662) @njuettner
|
||||
- dependencies: Upgrade all k8s client-go dependent sources to v1.18.X (#1627) @josephglanville
|
||||
- add GitHub Actions (#1657) @Raffo
|
||||
- add new source for istio virtual services (#1607) @tariq1890
|
||||
- use latest Alpine version in ExternalDNS dockerfile (#1655) @tariq1890
|
||||
- Update TTL docs to confirm DNSimple support (#1547) @weppos
|
||||
- rm unused flag param istio-ingressgateways (#1649) @tariq1890
|
||||
- Upgrade istio httpbin from 1.0 to 1.6 version (#1640) @ikovnatskymiacar
|
||||
- Add endpoints to kustomize base (#1638) @Raffo
|
||||
- DigitalOcean: support multiple targets per endpoint (#1595) @tdyas
|
||||
- Vultr : Version bump + changes (#1637) @ddymko
|
||||
- Hetzner DNS service support (#1570) @21h
|
||||
- Add OVH API rate limiting option (Fix #1546) (#1619) @Hugome
|
||||
- Add kustomize base (#1631) @Raffo
|
||||
- increase test timeout to fix intermittent failures of ingress tests (#1612) @tdyas
|
||||
- AWS: change the order of the actions, DELETE before CREATE fixes #1411 (#1555) @OmerKahani
|
||||
- Fix handling of DNS updates for RFC2136 provider. (#1613) @dmayle
|
||||
- digitalocean: increase API page size (#1611) @tdyas
|
||||
- improve linter quality for external-dns (#1618) @njuettner
|
||||
- fix convert int to string bug (#1620) @tariq1890
|
||||
|
||||
## v0.7.2 - 2020-06-03
|
||||
|
||||
- Update blogpost in README (#1610) @vanhumbeecka
|
||||
- Support for AWS Route53 in China (#1603) @greenu
|
||||
- Update Govcloud provider hosted zones (#1592) @clhuang
|
||||
- Fix issue with too large DNS messages (#1590) @dmayle
|
||||
- use the latest linode go version (#1587) @tariq1890
|
||||
- use istio client-go and clean up k8s deps (#1584) @tariq1890
|
||||
- Add owners for cloudflare and coredns providers (#1582) @Raffo
|
||||
- remove some code duplication in gateway source (#1575) @tariq1890
|
||||
- update Contour IngressRoute deps (#1569) @stevesloka
|
||||
- Make tests faster (#1568) @sheerun
|
||||
- Fix scheduling of reconciliation (#1567) @sheerun
|
||||
- fix minor typos in istio gateway source docs (#1566) @tariq1890
|
||||
- Provider structure refactor (#1565) @Raffo
|
||||
- Fix typo in ttl.md (#1564) @rtnpro
|
||||
- Fix goreportcard warnings (#1561) @squat
|
||||
- Use consistent headless service name in example (#1559) @lowkeyliesmyth
|
||||
- Update go versions to 1.14.x that were missed in commit 99cebfcf from PR #1476 (#1554) @stealthybox
|
||||
- Remove duplicate selector from DigitalOcean manifest (#1553) @ggordan
|
||||
- Upgrade DNSimple client and add support for contexts (#1551) @weppos
|
||||
- Upgrade github.com/miekg/dns to v1.1.25 (#1545) @davidcollom
|
||||
- Fix updates in CloudFlare provider (#1542) @sheerun
|
||||
- update readme for latest version (#1539) @elsesiy
|
||||
- Improve Cloudflare tests in preparation to fix other issues (#1537) @sheerun
|
||||
- Allow for custom property comparators (#1536) @sheerun
|
||||
- fix typo (#1535) @tmatias
|
||||
- Bump github.com/pkg/errors from 0.8.1 to 0.9.1 (#1531) @njuettner
|
||||
- Bump github.com/digitalocean/godo from 1.19.0 to 1.34.0 (#1530) @njuettner
|
||||
- Bump github.com/prometheus/client_golang from 1.0.0 to 1.5.1 (#1529) @njuettner
|
||||
- Bump github.com/akamai/AkamaiOPEN-edgegrid-golang from 0.9.10 to 0.9.11 (#1528) @njuettner
|
||||
- Fix RFC2316 Windows Documentation (#1516) @scottd018
|
||||
- remove dependency on kubernetes/kubernetes (#1513) @tariq1890
|
||||
- update akamai openapi dependency (#1511) @tariq1890
|
||||
- Vultr Provider (#1509) @ddymko
|
||||
- Add AWS region ap-east-1(HK) (#1497) @lovemai073
|
||||
- Fix: file coredns.go is not `goimports`-ed (#1496) @njuettner
|
||||
- Allow ZoneIDFilter for Cloudflare (#1494) @james-callahan
|
||||
- update etcd dependency to latest version (#1485) @tariq1890
|
||||
- Support for openshift routes (#1484) @jgrumboe
|
||||
- add --txt-suffix feature (#1483) @jgrumboe
|
||||
- update to go 1.14 (#1476) @jochen42
|
||||
- Multiple A records support for the same FQDN (#1475) @ytsarev
|
||||
- Implement annotation filter for CRD source (#1399) @ytsarev
|
||||
|
||||
## v0.7.1 - 2020-04-01
|
||||
|
||||
- Prometheus metric: timestamp of last successful sync with the DNS provider (#1480) @njuettner
|
||||
- Bump alpine base image to 3.11.5 (#1477) @Annegies
|
||||
- Docs: Add first maintainers in list (#1472) @Raffo
|
||||
- Fix DomainFilter type in OVH provider (#1469) @ytsarev
|
||||
- New provider: OVH (#1439) @Hugome
|
||||
|
||||
## v0.7.0 - 2020-03-10
|
||||
|
||||
- New source: Add support for Skipper's RouteGroup CRD (#1444) @szuecs
|
||||
- Change DomainFilter to apply to records as well (#1442) @bl1nk
|
||||
- Docs: Update docker image references of ExternalDNS (#1427) @tariq1890
|
||||
- Remove duplicate targets from endpoints for headless services (#1426) @thomasv314
|
||||
- Add issue templates to Github (#1424) @njuettner
|
||||
- Azure: Don't use SPN to authenticate when clientid or secret is 'msi' (#1422) @norshtein
|
||||
- Rfc2136: Add option to define minimum TTL (#1412) @ouzklcn
|
||||
- Azure Private DNS: Fix updates of unchanged records (#1377) @jasper-d
|
||||
- Headless service: Retrieve endpoints via Endpoints resource (#1005) @devkid
|
||||
|
||||
## v0.6.0 - 2020-02-11
|
||||
|
||||
- Azure Private DNS: Fix endless loop in zone-detection (#1397) @saidst
|
||||
- Uprade golangci-lint and add megacheck & interface linters (#1390) @tariq1890
|
||||
- Update alpine base image to 3.11 (#1387) @tariq1890
|
||||
- New provider: Akamai FastDNS (#1384) @KarstenSiemer
|
||||
- Docs: Fix broken links (#1382) @ttonline6
|
||||
- Docs: Fix broken links (#1381) @ttonline6
|
||||
- Docs: Update AWS documentation (#1380) @otterley
|
||||
- Docs: istio.md: update existing external-dns to enable Istio Gateway DNS for customers (#1378) @marcellodesales
|
||||
- Remove context.TODO()s in external-dns (#1374) @tariq1890
|
||||
- Docs: add region for aws-sd external-dns deployment (#1367) @guitarrapc
|
||||
- Docs: a how-to of a working GCP GKE app demo (#1365) @jpantsjoha
|
||||
- Add ctx parameter to provider interface and AWS API (#1364) @tariq1890
|
||||
- Add version to binary for --version flag (#1361) @linki
|
||||
- Update aws sdk dep and golangci-lint release (#1360) @tariq1890
|
||||
- Add support for human-friendly TTL values (#1237) @hypnoglow
|
||||
- Change ApplyChanges in RFC2136 to batch update (#1164) @h3ndrk
|
||||
- Add --watchers flag to allow controller to respond automatically to Ingress or Service updates (#687) @jlamillan
|
||||
|
||||
## v0.5.18 - 2020-01-09
|
||||
|
||||
- Use correct link to contributors guide (#1349) @szuecs
|
||||
- AWS-SD: Rebrand AWS Auto Naming to Cloud Map (#1348) @vanekjar
|
||||
- Add more linters and improve code quality (#1347) @tariq1890
|
||||
- Suppress noisy logging of klog (#1344) @saidst
|
||||
- Update VinylDNS documentation (#1342) @dgrizzanti
|
||||
- Remove incubator references in README (#1341) @Raffo
|
||||
- Rename project root package to sigs.k8s.io (#1334) @tariq1890
|
||||
- Add CRD documentation and fix samples (#1332) @ytsarev
|
||||
- Add support for multiple Istio ingress gateways (#1328) @ashleyschuett
|
||||
- Enable image publishing to gcr.io via cloudbuild (#1326) @njuettner
|
||||
- Corrected a typo in the Readme (#1323) @drewhemm
|
||||
- Rework tutorial for Azure Private DNS (#1319) @saidst
|
||||
- Correct typos and superflous spaces in the provider code (#1315) @stensonb
|
||||
- Add missing bracket in CLI help output (#1308) @ekeih
|
||||
- Add missing service account to deployment spec in the docs (#1305) @linki
|
||||
- Bump the version of golangci-lint (#1296) @njuettner
|
||||
- Fix broken link of ingress-gce and ingress-nginx (#1290) @sivanzcw
|
||||
- Use apps/v1 for the deployment to be compatible with Kubernetes 1.16 (#1279) @scholzj
|
||||
- Normalize function return and comments on exported type (#1277) @sivanzcw
|
||||
- Use non-deprecated initializer with go context (#1271) @linki
|
||||
- Fix several golint errors (#1270) @bysph
|
||||
- Add Azure Private DNS Provider (#1269) @saidst
|
||||
- Fix tutorial for kubernetes 1.16+ (#1268) @yujunz
|
||||
- Add me-south region to list of canonical hosted zones (#1266) @poweroftrue
|
||||
- Add gov region to list of canonical hosted zones (#1260) @helgi
|
||||
- Update broken links to RDNS (#1259) @Slach
|
||||
- Designate: add snippet for RBAC environment (#1254) @bavarianbidi
|
||||
- Fix log-level parameter in tutorials (#1253) @bavarianbidi
|
||||
- Improve RFC2136 documentation (#1251) @alex-orange
|
||||
- Google Provider: Add support for batching updates (#1248) @vdesjardins
|
||||
- Azure: add support for specifying user assigned identity's clientID to authenticate (#1247) @norshtein
|
||||
- Automatically add provider labels on pull requests via Github actions (#1242) @njuettner
|
||||
- Improve documentation for nginx ingress controller on AWS (#1234) @PiotrJander
|
||||
- Use apps/v1 instead of extensions/v1beta1 in Deployment examples (#1225) @reegnz
|
||||
- Add documentation to make the use of namespaces clearer (#1223) @dgrizzanti
|
||||
- Add support for using Nodes as Source (#1218) @skoef
|
||||
- Add missing RBAC permissions for the ServiceAccount in the docs (#1206) @dooman87
|
||||
- Upgrade client-go + azure sdk (#1195) @timja
|
||||
- RFC2136: Add support for batching updates (#1164) @h3ndrk
|
||||
- Fix confusing arrow direction in the Azure tutorial (#1163) @adipascu
|
||||
- Route53: Add RBAC manifest and update wording around IAM policy (#1149) @dkeightley
|
||||
- Route53: Add support for all AWS Route53 routing policies; add additional Setldentifier abstraction layer (#1008) @devkid
|
||||
|
||||
## v0.5.17 - 2019-09-17
|
||||
|
||||
- Exoscale: add context support (#1193) @greut
|
||||
- Cloudflare: Support API Token Auth (#1189) @Evesy
|
||||
- AWS: Fix IAM Roles for Service Accounts permission problem (#1185) @serialx
|
||||
- Core: Upgrade go version to 1.13 in external-dns (#1184) @tariq1890
|
||||
- AWS: Update the AWS SDK to support Web Identity providers for IAM credentials (#1182) @MarcusNoble
|
||||
- Docs: Update rfc2136 tutorial for use with Microsoft DNS (#1178) @bjschafer
|
||||
- AWS: Update the AWS go SDK to support AWS IAM for Service Accounts (#1172) @micahhausler
|
||||
- AWS-SD: Add support for AWS Network Load Balancers (#1170) @vanekjar
|
||||
- Core: Add create-only policy (#1160) @danieldabate
|
||||
- AWS: Fix --aws-api-retries (#1158) @coreypobrien
|
||||
- Source: Support delegate Heptio Contour IngressRoutes (#1144) @jonasrmichel
|
||||
- Core: TXTRegistry: do not overwrite labels of records returned by the provider (#1136) @multi-io
|
||||
- Infoblox: Fixing incorrect match of zone dns names (#1128) @gregsidelinger
|
||||
- Source: Improvements to the source CRD (#1107) @JoaoBraveCoding
|
||||
- Core: Fix txt prefix bug (#1013) @p53
|
||||
|
||||
## v0.5.16 - 2019-08-16
|
||||
|
||||
- Fix flaky unit test in provider package (#1151) @tariq1890
|
||||
- Dockerfile: Update version of base images (#1148) @tariq1890
|
||||
- DigitalOcean: Update `godo` to the latest stable version (#1145) @tariq1890
|
||||
- Fix build pipeline for Go v1.13 (#1142) @linki
|
||||
- AWS: Add Hosted Zone ID to logging output (#1129) @helgi
|
||||
- IstioGateway: Support namespaces on hostnames (#1124) @dcherman
|
||||
- AWS: Document `--prefer-cname` flag (#1123) @dbluxo
|
||||
- Add Tutorial for DNSimple provider (#1121) @marc-sensenich
|
||||
- Update Go version and golangci-lint to the latest release (#1120) @njuettner
|
||||
- Allow compilation on 32bit machines (#1116) @mylesagray
|
||||
- AWS: Allow to force usage of CNAME over ALIAS (#1103) @linki
|
||||
- CoreDNS: add option to specify prefix name (#1102) @xunpan
|
||||
- New provider: Rancher DNS (RDNS) (#1098) @Jason-ZW
|
||||
- Document where e2e tests are currently located (#1094) @jaypipes
|
||||
- Add initial KEP for ExternalDNS (#1092) @Raffo
|
||||
- Update Dockerfiles to follow best practices (#1091) @taharah
|
||||
- New Source: Heptio Contour IngressRoute (#1084) @jonasrmichel
|
||||
- AWS: Add dualstack support with ALB ingress controllers (#1079) @twilfong
|
||||
- Allow handling of multiple Oracle Cloud (OCI) zones (#1061) @suman-ganta
|
||||
- Namespace exposed metrics with the external_dns prefix (#794) @linki
|
||||
|
||||
## v0.5.15 - 2019-07-03
|
||||
|
||||
- RFC2136: Fix when merging multiple targets (#1082) @hachh
|
||||
- New provider VinylDNS (#1080) @dgrizzanti
|
||||
- Core: Fix for DomainFilter exclusions (#1059) @cmattoon
|
||||
- Core: Update aws-go-sdk to be compatible with kube-aws-iam-controller (#1054) @mikkeloscar
|
||||
- RFC2136: Log RR adds/deletes as Info (#1041) @gclawes
|
||||
- Docs: Cloudflare set ttl annotation for proxied entries to 1 (#1039) @MiniJerome
|
||||
- Core: Install ca-certificates (#1038) @dryewo
|
||||
- Cloudflare: Fix provider to return a single endpoint for each name/type (#1034) @shasderias
|
||||
- Core: Sanitize dockerfiles for external-dns (#1033) @tariq1890
|
||||
- Core: Add empty source (#1032) @anandkumarpatel
|
||||
- Google: Zones should be filter by their ID and Name (#1031) @simonswine
|
||||
- Core: Fix panic on empty targets for custom resources (#1029) @arturo-c
|
||||
- Core: Support externalTrafficPolicy annotation with "local" mode for NodePort service (#1023) @yverbin
|
||||
- Core: Add support for ExternalName services (#1018) @mironov
|
||||
|
||||
## v0.5.14 - 2019-05-14
|
||||
|
||||
- Docs: Update aws.md (#1009) @pawelprazak
|
||||
- New provider TransIP (#1007) @skoef
|
||||
- Docs: Add docker image faq (#1006) @Raffo
|
||||
- DNSimple: Support apex records (#1004) @jbowes
|
||||
- NS1: Add --ns1-endpoint and --ns1-ignoressl flags (#1002) @mburtless
|
||||
- AWS: Cache the endpoints on the controller loop (#1001) @fraenkel
|
||||
- Core: Supress Kubernetes logs (#991) @njuettner
|
||||
- Core: distroless/static image (#989) @jharshman
|
||||
- Core: Headless service missing DNS entry (#984) @yverbin
|
||||
- New provider NS1 (#963) @mburtless
|
||||
- Core: Add Cloud Foundry routes as a source (#955) @dgrizzanti
|
||||
|
||||
## v0.5.13 - 2019-04-18
|
||||
|
||||
- Azure: Support multiple A targets (#987) @michaelfig
|
||||
- Core: Fixing what seems an obvious omission of /github.com/ dir in Dockerfile (#985) @llamahunter
|
||||
- Docs: GKE tutorial remove disable-addon argument (#978) @ggordan
|
||||
- Docs: Alibaba Cloud config file missing by enable sts token (#977) @xianlubird
|
||||
- Docs: Alibaba Cloud fix wrong arg in manifest (#976) @iamzhout
|
||||
- AWS: Set a default TTL for Alias records (#975) @fraenkel
|
||||
- Cloudflare: Add support for multiple target addresses (#970) @nta
|
||||
- AWS: Adding China ELB endpoints and hosted zone id's (#968) @jfillo
|
||||
- AWS: Streamline ApplyChanges (#966) @fraenkel
|
||||
- Core: Switch to go modules (#960) @njuettner
|
||||
- Docs: AWS how to check if your cluster has a RBAC (#959) @confiq
|
||||
- Docs: AWS remove superfluous trailing period from hostname (#952) @hobti01
|
||||
- Core: Add generic logic to remove secrets from logs (#951) @dsbrng25b
|
||||
- RFC2136: Remove unnecessary parameter (#948) @ChristianMoesl
|
||||
- Infoblox: Reduce verbosity of logs (#945) @dsbrng25b
|
||||
|
||||
## v0.5.12 - 2019-03-26
|
||||
|
||||
- Bumping istio to 1.1.0 (#942) @venezia
|
||||
- Docs: Added stability matrix and minor improvements to README (#938) @Raffo
|
||||
- Docs: Added a reference to a blogpost which uses ExternalDNS in a CI/CD setup (#928) @vanhumbeecka
|
||||
- Use k8s informer cache instead of making active API GET requests (#917) @jlamillan
|
||||
- Docs: Tiny clarification about two available deployment methods (#935) @przemolb
|
||||
- Add support for multiple Istio IngressGateway LoadBalancer Services (#907) @LorbusChris
|
||||
- Set log level to debug when axfr is disabled (#932) @arief-hidayat
|
||||
- Infoblox provider support for DNS view (#895) @dsbrng25b
|
||||
- Add RcodeZero Anycast DNS provider (#874) @dklesev
|
||||
- Docs: Dropping owners (#929) @njuettner
|
||||
- Docs: Added description for multiple dns name (#911) @st1t
|
||||
- Docs: Clarify that hosted zone identifier is to be used (#915) @dirkgomez
|
||||
- Docs: Make dep step which may be needed to run make build (#913) @dirkgomez
|
||||
- PowerDNS: Fixed Domain Filter Bug (#827) @anandsinghkunwar
|
||||
- Allow hostname annotations to be ignored (#745) @anandkumarpatel
|
||||
- RFC2136: Fixed typo in debug output (#899) @hpandeycodeit
|
||||
|
||||
## v0.5.11 - 2019-02-11
|
||||
|
||||
- Fix constant updating issue introduced with v0.5.10 (#886) @jhohertz
|
||||
- Ignore evaluate target health for calculating changes for AWS (#880) @linki
|
||||
- Pagination for cloudflare zones (#873) @njuettner
|
||||
|
||||
## v0.5.10 - 2019-01-28
|
||||
|
||||
- Docs: Improve documentation regarding Alias (#868) @alexnederlof
|
||||
- Adds a new flag `--aws-api-retries` which allows overriding the number of retries (#858) @viafoura
|
||||
- Docs: Make awscli commands use JSON output (#849) @ifosch
|
||||
- Docs: Add missing apiVersion to Ingress resource (#847) @shlao
|
||||
- Fix for AWS private DNS zone (#844) @xianlubird
|
||||
- Add support for AWS ELBs in eu-north-1 (#843) @argoyle
|
||||
- Create a SECURITY_CONTACTS file (#842) @njuettner
|
||||
- Use correct product name for Google Cloud DNS (#841) @seils
|
||||
- Change default AWSBatchChangeSize to 1000 (#839) @medzin
|
||||
- Fix dry-run mode in rfc2136 provider (#838) @lachlancooper
|
||||
- Fix typos in rfc2136 provider (#837) @lachlancooper
|
||||
- rfc2136 provider: one IP Target per RRSET (#836) @ivanfilippov
|
||||
- Normalize DNS names during planning (#833) @justinsb
|
||||
- Implement Stringer for planTableRow (#832) @justinsb
|
||||
- Docs: Better security granularity concerning external dns service principal for Azure (#829) @DenisBiondic
|
||||
- Docs: Update links in Cloudflare docs (#824) @PascalKu
|
||||
- Docs: Add metrics info to FAQ (#822) @zachyam
|
||||
- Docs: Update nameserver IPs in coredns.md (#820) @mozhuli
|
||||
- Docs: Fix commands to cleanup Cloudflare (#818) @acrogenesis
|
||||
- Avoid unnecessary updating for CRD resource (#810) @xunpan
|
||||
- Fix issues with CoreDNS provider and more than 1 targets (#807) @xunpan
|
||||
- AWS: Add zone tag filter (#804) @csrwng
|
||||
- Docs: Update CoreDNS tutorial with RBAC manifest (#803) @Lujeni
|
||||
- Use SOAP API to improve DYN's provider's performance (#799) @sanyu
|
||||
- Expose managed resources and records as metrics (#793) @linki
|
||||
- Docs: Updating Azure tutorial (#788) @pelithne
|
||||
- Improve errors in Records() of Infoblox provider (#785) @dsbrng25b
|
||||
- Change default apiVersion of CRD Source (#774) @dsbrng25b
|
||||
- Allow setting Cloudflare proxying on a per-Ingress basis (#650) @eswets
|
||||
- Support A record for multiple IPs for headless services (#645) @toshipp
|
||||
|
||||
## v0.5.9 - 2018-11-22
|
||||
|
||||
- Core: Update delivery.yaml to new format (#782) @linki
|
||||
- Core: Adjust gometalinter timeout by setting env var (#778) @njuettner
|
||||
- Provider **Google**: Panic assignment to entry in nil map (#776) @njuettner
|
||||
- Docs: Fix typos (#769) @mooncak
|
||||
- Docs: Remove duplicated words (#768) @mooncak
|
||||
- Provider **Alibaba**: Alibaba Cloud Provider Fix Multiple Subdomains Bug (#767) @xianlubird
|
||||
- Core: Add Traefik to the supported list of ingress controllers (#764) @coderanger
|
||||
- Provider **Dyn**: Fix some typos in returned messages in dyn.go (#760) @AdamDang
|
||||
- Docs: Update Azure documentation (#756) @pascalgn
|
||||
- Provider **Oracle**: Oracle doc fix (add "key:" to secret) (#750) @CaptTofu
|
||||
- Core: Docker MAINTAINER is deprecated - using LABEL instead (#747) @helgi
|
||||
- Core: Feature add alias annotation (#742) @vaegt
|
||||
- Provider **RFC2136**: Fix rfc2136 - setup fails issue and small docs (#741) @antlad
|
||||
- Core: Fix nil map access of endpoint labels (#739) @shashidharatd
|
||||
- Provider **PowerDNS**: PowerDNS Add DomainFilter support (#737) @ottoyiu
|
||||
- Core: Fix domain-filter matching logic to not match similar domain names (#736) @ottoyiu
|
||||
- Core: Matching entire string for wildcard in txt records with prefixes (#727) @etopeter
|
||||
- Provider **Designate**: Fix TLS issue with OpenStack auth (#717) @FestivalBobcats
|
||||
- Provider **AWS**: Add helper script to update route53 txt owner entries (#697) @efranford
|
||||
- Provider **CoreDNS**: Migrate to use etcd client v3 for CoreDNS provider (#686) @shashidharatd
|
||||
- Core: Create a non-root user to run the container process (#684) @coderanger
|
||||
- Core: Do not replace TXT records with A/CNAME records in planner (#581) @jchv
|
||||
|
||||
## v0.5.8 - 2018-10-11
|
||||
|
||||
- New Provider: RFC2136 (#702) @antlad
|
||||
- Add Linode to list of supported providers (#730) @cliedeman
|
||||
- Correctly populate target health check on existing records (#724) @linki
|
||||
- Don't erase Endpoint labels (#713) @sebastien-prudhomme
|
||||
|
||||
## v0.5.7 - 2018-09-27
|
||||
|
||||
- Pass all relevant CLI flags to AWS provider (#719) @linki
|
||||
- Replace glog with a noop logger (#714) @linki
|
||||
- Fix handling of custom TTL values with Google DNS. (#704) @kevinmdavis
|
||||
- Continue even if node listing fails (#701) @pascalgn
|
||||
- Fix Host field in HTTP request when using pdns provider (#700) @peterbale
|
||||
- Allow AWS batching to fully sync on each run (#699) @bartelsielski
|
||||
|
||||
## v0.5.6 - 2018-09-07
|
||||
|
||||
- Alibaba Cloud (#696) @xianlubird
|
||||
- Add Source implementation for Istio Gateway (#694) @jonasrmichel
|
||||
- CRD source based on getting endpoints from CRD (#657) @shashidharatd
|
||||
- Add filter by service type feature (#653) @Devatoria
|
||||
- Add generic metrics for Source & Registry Errors (#652) @wleese
|
||||
|
||||
## v0.5.5 - 2018-08-17
|
||||
|
||||
- Configure req timeout calling k8s APIs (#681) @jvassev
|
||||
- Adding assume role to aws_sd provider (#676) @lb-saildrone
|
||||
- Dyn: cache records per zone using zone's serial number (#675) @jvassev
|
||||
- Linode provider (#674) @cliedeman
|
||||
- Cloudflare Link Language Specificity (#673) @christopherhein
|
||||
- Retry calls to dyn on ErrRateLimited (#671) @jvassev
|
||||
- Add support to configure TTLs on DigitalOcean (#667) @andrewsomething
|
||||
- Log level warning option (#664) @george-angel
|
||||
- Fix usage of k8s.io/client-go package (#655) @shashidharatd
|
||||
- Fix for empty target annotation (#647) @rdrgmnzs
|
||||
- Fix log message for #592 when no updates in hosted zones (#634) @audip
|
||||
- Add aws-evaluate-target-health flag (#628) @peterbale
|
||||
- Exoscale provider (#625) @FaKod @greut
|
||||
- Oracle Cloud Infrastructure DNS provider (#626) @prydie
|
||||
- Update DO CNAME type API request to prevent error 422 (#624) @nenadilic84
|
||||
- Fix typo in cloudflare.md (#623) @derekperkins
|
||||
- Infoblox-go-client was only setting timeout for http.Transport.ResponseHeaderTimeout instead of for http.Client (#615) @khrisrichardson
|
||||
- Adding a flag to optionally publish hostIP instead of podIP for headless services (#597) @Arttii
|
||||
|
||||
## 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
|
||||
- Document how ExternalDNS gets permission to change AWS Route53 entries (#557) @hjacobs
|
||||
- Fix CNAME support for the PowerDNS provider (#547) @kciredor
|
||||
- Add support for hostname annotation in Ingress resource (#545) @rajatjindal
|
||||
- Fix for TTLs being ignored on headless Services (#546) @danbondd
|
||||
- Fix failing tests by giving linters more time to do their work (#548) @linki
|
||||
- Fix misspelled flag for the OpenStack Designate provider (#542) @zentale
|
||||
- Document additional RBAC rules needed to read Pods (#538) @danbondd
|
||||
|
||||
## v0.5.0 - 2018-04-23
|
||||
|
||||
- Google: Correctly filter records that don't match all filters (#533) @prydie @linki
|
||||
- AWS: add support for AWS Network Load Balancers (#531) @linki
|
||||
- Add a flag that allows FQDN template and annotations to combine (#513) @helgi
|
||||
- Fix: Use PodIP instead of HostIP for headless Services (#498) @nrobert13
|
||||
- Support a comma separated list for the FQDN template (#512) @helgi
|
||||
- Google Provider: Add auto-detection of Google Project when running on GCP (#492) @drzero42
|
||||
- Add custom TTL support for DNSimple (#477) @jbowes
|
||||
- Fix docker build and delete vendor files which were not deleted (#473) @njuettner
|
||||
- DigitalOcean: DigitalOcean creates entries with host in them twice (#459) @njuettner
|
||||
- Bugfix: Retrive all DNSimple response pages (#468) @jbowes
|
||||
- external-dns does now provide support for multiple targets for A records. This is currently only supported by the Google Cloud DNS provider (#418) @dereulenspiegel
|
||||
- Graceful handling of misconfigure password for dyn provider (#470) @jvassev
|
||||
- Don't log sensitive data on start (#463) @jvassev
|
||||
- Google: Improve logging to help trace misconfigurations (#388) @stealthybox
|
||||
- AWS: In addition to the one best public hosted zone, records will be added to all matching private hosted zones (#356) @coreypobrien
|
||||
- Every record managed by External DNS is now mapped to a kubernetes resource (service/ingress) @ideahitme
|
||||
- New field is stored in TXT DNS record which reflects which kubernetes resource has acquired the DNS name
|
||||
- Target of DNS record is changed only if corresponding kubernetes resource target changes
|
||||
- If kubernetes resource is deleted, then another resource may acquire DNS name
|
||||
- "Flapping" target issue is resolved by providing a consistent and defined mechanism for choosing a target
|
||||
- New `--zone-id-filter` parameter allows filtering by zone id (#422) @vboginskey
|
||||
- TTL annotation check for azure records (#436) @stromming
|
||||
- Switch from glide to dep (#435) @bkochendorfer
|
||||
|
||||
## v0.4.8 - 2017-11-22
|
||||
|
||||
- Allow filtering by source annotation via `--annotation-filter` (#354) @khrisrichardson
|
||||
- Add support for Headless hostPort services (#324) @Arttii
|
||||
- AWS: Added change batch limiting to a maximum of 4000 Route53 updates in one API call. Changes exceeding the limit will be dropped but all related changes by hostname are preserved within the limit. (#368) @bitvector2
|
||||
- Google: Support configuring TTL by annotation: `external-dns.alpha.kubernetes.io/ttl`. (#389) @stealthybox
|
||||
- Infoblox: add option `--no-infoblox-ssl-verify` (#378) @khrisrichardson
|
||||
- Inmemory: add support to specify zones for inmemory provider via command line (#366) @ffledgling
|
||||
|
||||
## v0.4.7 - 2017-10-18
|
||||
|
||||
- CloudFlare: Disable proxy mode for TXT and others (#361) @dunglas
|
||||
|
||||
## v0.4.6 - 2017-10-12
|
||||
|
||||
- [AWS Route53 provider] Support customization of DNS record TTL through the use of annotation `external-dns.alpha.kubernetes.io/ttl` on services or ingresses (#320) @kevinjqiu
|
||||
- Added support for [DNSimple](https://dnsimple.com/) as DNS provider (#224) @jose5918
|
||||
- Added support for [Infoblox](https://www.infoblox.com/products/dns/) as DNS provider (#349) @khrisrichardson
|
||||
|
||||
## v0.4.5 - 2017-09-24
|
||||
|
||||
- Add `--log-level` flag to control log verbosity and remove `--debug` flag in favour of `--log-level=debug` (#339) @ultimateboy
|
||||
- AWS: Allow filtering for private and public zones via `--aws-zone-type` flag (#329) @linki
|
||||
- CloudFlare: Add `--cloudflare-proxied` flag to toggle CloudFlare proxy feature (#340) @dunglas
|
||||
- Kops Compatibility: Isolate ALIAS type in AWS provider (#248) @sethpollack
|
||||
|
||||
## v0.4.4 - 2017-08-17
|
||||
|
||||
- ExternalDNS now services of type `ClusterIP` with the use of the `--publish-internal-services`. Enabling this will now create the apprioriate A records for the given service's internal ip. @jrnt30
|
||||
- Fix to have external target annotations on ingress resources replace existing endpoints instead of appending to them (#318)
|
||||
|
||||
## v0.4.3 - 2017-08-10
|
||||
|
||||
- Support new `external-dns.alpha.kubernetes.io/target` annotation for Ingress (#312)
|
||||
- Fix for wildcard domains in Route53 (#302)
|
||||
|
||||
## v0.4.2 - 2017-08-03
|
||||
|
||||
- Fix to support multiple hostnames for Molecule Software's [route53-kubernetes](https://github.com/wearemolecule/route53-kubernetes) compatibility (#301)
|
||||
|
||||
## v0.4.1 - 2017-07-28
|
||||
|
||||
- Fix incorrect order of constructor parameters (#298)
|
||||
|
||||
## v0.4.0 - 2017-07-21
|
||||
|
||||
- ExternalDNS now supports three more DNS providers:
|
||||
* [AzureDNS](https://azure.microsoft.com/en-us/services/dns) @peterhuene
|
||||
* [CloudFlare](https://www.cloudflare.com/de/dns) @njuettner
|
||||
* [DigitalOcean](https://www.digitalocean.com/products/networking) @njuettner
|
||||
- Fixed a bug that prevented ExternalDNS to be run on Tectonic clusters @sstarcher
|
||||
- ExternalDNS is now a full replace for Molecule Software's `route53-kubernetes` @iterion
|
||||
- The `external-dns.alpha.kubernetes.io/hostname` annotation accepts now a comma separated list of hostnames and a trailing period is not required anymore. @totallyunknown
|
||||
- The flag `--domain-filter` can be repeated multiple times like `--domain-filter=example.com --domain-filter=company.org.`. @totallyunknown
|
||||
- A trailing period is not required anymore for `--domain-filter` when AWS (or any other) provider is used. @totallyunknown
|
||||
- We added a FakeSource that generates random endpoints and allows to run ExternalDNS without a Kubernetes cluster (e.g. for testing providers) @ismith
|
||||
- All HTTP requests to external APIs (e.g. DNS providers) generate client side metrics. @linki
|
||||
- The `--zone` parameter was removed in favor of a provider independent `--domain-filter` flag. @linki
|
||||
- All flags can now also be set via environment variables. @linki
|
||||
|
||||
## v0.3.0 - 2017-05-08
|
||||
|
||||
Features:
|
||||
|
||||
- Changed the flags to the v0.3 semantics, the following has changed:
|
||||
1. The TXT registry is used by default and has an owner ID of `default`
|
||||
2. `--dry-run` is disabled by default
|
||||
3. The `--compatibility` flag was added and takes a string instead of a boolean
|
||||
4. The `--in-cluster` flag has been dropped for auto-detection
|
||||
5. The `--zone` specifier has been replaced by a `--domain-filter` that filters domains by suffix
|
||||
- Improved logging output
|
||||
- Generate DNS Name from template for services/ingress if annotation is missing but `--fqdn-template` is specified
|
||||
- Route 53, Google CloudDNS: Support creation of records in multiple hosted zones.
|
||||
- Route 53: Support creation of ALIAS records when endpoint target is a ELB/ALB.
|
||||
- Ownership via TXT records
|
||||
1. Create TXT records to mark the records managed by External DNS
|
||||
2. Supported for AWS Route53 and Google CloudDNS
|
||||
3. Configurable TXT record DNS name format
|
||||
- Add support for altering the DNS record modification behavior via policies.
|
||||
|
||||
## v0.2.0 - 2017-04-07
|
||||
|
||||
Features:
|
||||
|
||||
- Support creation of CNAME records when endpoint target is a hostname.
|
||||
- Allow omitting the trailing dot in Service annotations.
|
||||
- Expose basic Go metrics via Prometheus.
|
||||
|
||||
Documentation:
|
||||
|
||||
- Add documentation on how to setup ExternalDNS for Services on AWS.
|
||||
|
||||
## v0.1.1 - 2017-04-03
|
||||
|
||||
Bug fixes:
|
||||
|
||||
- AWS Route 53: Do not submit request when there are no changes.
|
||||
|
||||
## v0.1.0 - 2017-03-30 (KubeCon)
|
||||
|
||||
Features:
|
||||
|
||||
- Manage DNS records for Services with `Type=LoadBalancer` on Google CloudDNS.
|
10
Dockerfile
10
Dockerfile
@ -14,16 +14,20 @@
|
||||
|
||||
# builder image
|
||||
ARG ARCH
|
||||
FROM golang:1.15 as builder
|
||||
FROM golang:1.16 as builder
|
||||
ARG ARCH
|
||||
|
||||
WORKDIR /sigs.k8s.io/external-dns
|
||||
|
||||
COPY go.mod .
|
||||
COPY go.sum .
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
RUN make test && make build.$ARCH
|
||||
RUN make test build.$ARCH
|
||||
|
||||
# final image
|
||||
FROM $ARCH/alpine:3.12
|
||||
FROM $ARCH/alpine:3.13
|
||||
|
||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
COPY --from=builder /sigs.k8s.io/external-dns/build/external-dns /bin/external-dns
|
||||
|
@ -12,17 +12,21 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
FROM golang:1.15 as builder
|
||||
FROM golang:1.16 as builder
|
||||
|
||||
WORKDIR /sigs.k8s.io/external-dns
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install \
|
||||
ca-certificates \
|
||||
&& update-ca-certificates
|
||||
|
||||
COPY go.mod .
|
||||
COPY go.sum .
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
RUN apt-get update && \
|
||||
apt-get install ca-certificates && \
|
||||
update-ca-certificates && \
|
||||
go mod vendor && \
|
||||
make test && \
|
||||
make build
|
||||
RUN make test build
|
||||
|
||||
FROM gcr.io/distroless/static
|
||||
|
||||
|
25
Makefile
25
Makefile
@ -27,6 +27,23 @@ cover:
|
||||
cover-html: cover
|
||||
go tool cover -html cover.out
|
||||
|
||||
# find or download controller-gen
|
||||
# download controller-gen if necessary
|
||||
controller-gen:
|
||||
ifeq (, $(shell which controller-gen))
|
||||
@{ \
|
||||
set -e ;\
|
||||
CONTROLLER_GEN_TMP_DIR=$$(mktemp -d) ;\
|
||||
cd $$CONTROLLER_GEN_TMP_DIR ;\
|
||||
go mod init tmp ;\
|
||||
go get sigs.k8s.io/controller-tools/cmd/controller-gen@v0.5.0 ;\
|
||||
rm -rf $$CONTROLLER_GEN_TMP_DIR ;\
|
||||
}
|
||||
CONTROLLER_GEN=$(GOBIN)/controller-gen
|
||||
else
|
||||
CONTROLLER_GEN=$(shell which controller-gen)
|
||||
endif
|
||||
|
||||
.PHONY: go-lint
|
||||
|
||||
# Run the golangci-lint tool
|
||||
@ -51,6 +68,11 @@ licensecheck:
|
||||
# Run all the linters
|
||||
lint: licensecheck go-lint
|
||||
|
||||
.PHONY: crd
|
||||
|
||||
# generates CRD using controller-gen
|
||||
crd: controller-gen
|
||||
${CONTROLLER_GEN} crd:crdVersions=v1 paths="./endpoint/..." output:crd:stdout > docs/contributing/crd-source/crd-manifest.yaml
|
||||
|
||||
# The verify target runs tasks similar to the CI tasks, but without code coverage
|
||||
.PHONY: verify test
|
||||
@ -82,7 +104,8 @@ build.push/multiarch:
|
||||
for arch in $(ARCHS); do \
|
||||
image="$(IMAGE):$(VERSION)-$${arch}" ;\
|
||||
# pre-pull due to https://github.com/kubernetes-sigs/cluster-addons/pull/84/files ;\
|
||||
docker pull $${arch}/alpine:3.12 ;\
|
||||
docker pull $${arch}/alpine:3.13 ;\
|
||||
docker pull golang:1.16 ;\
|
||||
DOCKER_BUILDKIT=1 docker build --rm --tag $${image} --build-arg VERSION="$(VERSION)" --build-arg ARCH="$${arch}" . ;\
|
||||
docker push $${image} ;\
|
||||
arch_specific_tags+=( "--amend $${image}" ) ;\
|
||||
|
5
OWNERS
5
OWNERS
@ -5,6 +5,11 @@ approvers:
|
||||
- raffo
|
||||
- njuettner
|
||||
|
||||
reviewers:
|
||||
- njuettner
|
||||
- raffo
|
||||
- seanmalloy
|
||||
|
||||
emeritus_approvers:
|
||||
- hjacobs
|
||||
- linki
|
||||
|
36
README.md
36
README.md
@ -21,13 +21,14 @@ The [FAQ](docs/faq.md) contains additional information and addresses several que
|
||||
|
||||
To see ExternalDNS in action, have a look at this [video](https://www.youtube.com/watch?v=9HQ2XgL9YVI) or read this [blogpost](https://codemine.be/posts/20190125-devops-eks-externaldns/).
|
||||
|
||||
## The Latest Release: v0.7
|
||||
## The Latest Release: v0.8
|
||||
|
||||
ExternalDNS' current release is `v0.7`. This version allows you to keep selected zones (via `--domain-filter`) synchronized with Ingresses and Services of `type=LoadBalancer` in various cloud providers:
|
||||
ExternalDNS' current release is `v0.8`. This version allows you to keep selected zones (via `--domain-filter`) synchronized with Ingresses and Services of `type=LoadBalancer` in various cloud providers:
|
||||
* [Google Cloud DNS](https://cloud.google.com/dns/docs/)
|
||||
* [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)
|
||||
@ -49,6 +50,8 @@ ExternalDNS' current release is `v0.7`. This version allows you to keep selected
|
||||
* [OVH](https://www.ovh.com)
|
||||
* [Scaleway](https://www.scaleway.com)
|
||||
* [Akamai Edge DNS](https://learn.akamai.com/en-us/products/cloud_security/edge_dns.html)
|
||||
* [GoDaddy](https://www.godaddy.com)
|
||||
* [Gandi](https://www.gandi.net)
|
||||
|
||||
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.
|
||||
|
||||
@ -81,6 +84,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 | |
|
||||
@ -103,6 +107,8 @@ The following table clarifies the current status of the providers according to t
|
||||
| Scaleway DNS | Alpha | @Sh4d1 |
|
||||
| Vultr | Alpha | |
|
||||
| UltraDNS | Alpha | |
|
||||
| GoDaddy | Alpha | |
|
||||
| Gandi | Alpha | @packi |
|
||||
|
||||
## Running ExternalDNS:
|
||||
|
||||
@ -125,6 +131,7 @@ The following tutorials are provided:
|
||||
* [Azure DNS](docs/tutorials/azure.md)
|
||||
* [Azure Private DNS](docs/tutorials/azure-private-dns.md)
|
||||
* [Cloudflare](docs/tutorials/cloudflare.md)
|
||||
* [BlueCat](docs/tutorials/bluecat.md)
|
||||
* [CoreDNS](docs/tutorials/coredns.md)
|
||||
* [DigitalOcean](docs/tutorials/digitalocean.md)
|
||||
* [Hetzner](docs/tutorials/hetzner.md)
|
||||
@ -155,6 +162,8 @@ The following tutorials are provided:
|
||||
* [Scaleway](docs/tutorials/scaleway.md)
|
||||
* [Vultr](docs/tutorials/vultr.md)
|
||||
* [UltraDNS](docs/tutorials/ultradns.md)
|
||||
* [GoDaddy](docs/tutorials/godaddy.md)
|
||||
* [Gandi](docs/tutorials/gandi.md)
|
||||
|
||||
### Running Locally
|
||||
|
||||
@ -166,20 +175,20 @@ from source.
|
||||
Next, run an application and expose it via a Kubernetes Service:
|
||||
|
||||
```console
|
||||
$ kubectl run nginx --image=nginx --port=80
|
||||
$ kubectl expose pod nginx --port=80 --target-port=80 --type=LoadBalancer
|
||||
kubectl run nginx --image=nginx --port=80
|
||||
kubectl expose pod nginx --port=80 --target-port=80 --type=LoadBalancer
|
||||
```
|
||||
|
||||
Annotate the Service with your desired external DNS name. Make sure to change `example.org` to your domain.
|
||||
|
||||
```console
|
||||
$ kubectl annotate service nginx "external-dns.alpha.kubernetes.io/hostname=nginx.example.org."
|
||||
kubectl annotate service nginx "external-dns.alpha.kubernetes.io/hostname=nginx.example.org."
|
||||
```
|
||||
|
||||
Optionally, you can customize the TTL value of the resulting DNS record by using the `external-dns.alpha.kubernetes.io/ttl` annotation:
|
||||
|
||||
```console
|
||||
$ kubectl annotate service nginx "external-dns.alpha.kubernetes.io/ttl=10"
|
||||
kubectl annotate service nginx "external-dns.alpha.kubernetes.io/ttl=10"
|
||||
```
|
||||
|
||||
For more details on configuring TTL, see [here](docs/ttl.md).
|
||||
@ -187,7 +196,7 @@ For more details on configuring TTL, see [here](docs/ttl.md).
|
||||
Use the internal-hostname annotation to create DNS records with ClusterIP as the target.
|
||||
|
||||
```console
|
||||
$ kubectl annotate service nginx "external-dns.alpha.kubernetes.io/internal-hostname=nginx.internal.example.org."
|
||||
kubectl annotate service nginx "external-dns.alpha.kubernetes.io/internal-hostname=nginx.internal.example.org."
|
||||
```
|
||||
|
||||
If the service is not of type Loadbalancer you need the --publish-internal-services flag.
|
||||
@ -195,7 +204,7 @@ If the service is not of type Loadbalancer you need the --publish-internal-servi
|
||||
Locally run a single sync loop of ExternalDNS.
|
||||
|
||||
```console
|
||||
$ external-dns --registry txt --txt-owner-id my-cluster-id --provider google --google-project example-project --source service --once --dry-run
|
||||
external-dns --registry txt --txt-owner-id my-cluster-id --provider google --google-project example-project --source service --once --dry-run
|
||||
```
|
||||
|
||||
This should output the DNS records it will modify to match the managed zone with the DNS records you desire. It also assumes you are running in the `default` namespace. See the [FAQ](docs/faq.md) for more information regarding namespaces.
|
||||
@ -205,13 +214,13 @@ Note: TXT records will have `my-cluster-id` value embedded. Those are used to en
|
||||
Once you're satisfied with the result, you can run ExternalDNS like you would run it in your cluster: as a control loop, and **not in dry-run** mode:
|
||||
|
||||
```console
|
||||
$ external-dns --registry txt --txt-owner-id my-cluster-id --provider google --google-project example-project --source service
|
||||
external-dns --registry txt --txt-owner-id my-cluster-id --provider google --google-project example-project --source service
|
||||
```
|
||||
|
||||
Check that ExternalDNS has created the desired DNS record for your Service and that it points to its load balancer's IP. Then try to resolve it:
|
||||
|
||||
```console
|
||||
$ dig +short nginx.example.org.
|
||||
dig +short nginx.example.org.
|
||||
104.155.60.49
|
||||
```
|
||||
|
||||
@ -271,12 +280,15 @@ Here's a rough outline on what is to come (subject to change):
|
||||
|
||||
### v0.6
|
||||
|
||||
- [ ] Ability to replace Kops' [DNS Controller](https://github.com/kubernetes/kops/tree/HEAD/dns-controller) (This could also directly become `v1.0`)
|
||||
- [ ] Ability to replace kOps' [DNS Controller](https://github.com/kubernetes/kops/tree/HEAD/dns-controller) (This could also directly become `v1.0`)
|
||||
- [x] Support for OVH
|
||||
|
||||
### v1.0
|
||||
|
||||
- [ ] Ability to replace Kops' [DNS Controller](https://github.com/kubernetes/kops/tree/HEAD/dns-controller)
|
||||
- [ ] Ability to replace kOps' [DNS Controller](https://github.com/kubernetes/kops/tree/HEAD/dns-controller)
|
||||
- [x] Add support for pod source
|
||||
- [x] Add support for DNS Controller annotations for pod and service sources
|
||||
- [ ] Add support for kOps gossip provider
|
||||
- [x] Ability to replace Zalando's [Mate](https://github.com/linki/mate)
|
||||
- [x] Ability to replace Molecule Software's [route53-kubernetes](https://github.com/wearemolecule/route53-kubernetes)
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
# See https://cloud.google.com/cloud-build/docs/build-config
|
||||
timeout: 3000s
|
||||
timeout: 5000s
|
||||
options:
|
||||
substitution_option: ALLOW_LOOSE
|
||||
steps:
|
||||
|
@ -72,6 +72,14 @@ var (
|
||||
Help: "Timestamp of last successful sync with the DNS provider",
|
||||
},
|
||||
)
|
||||
controllerNoChangesTotal = prometheus.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Namespace: "external_dns",
|
||||
Subsystem: "controller",
|
||||
Name: "no_op_runs_total",
|
||||
Help: "Number of reconcile loops ending up with no changes on the DNS provider side.",
|
||||
},
|
||||
)
|
||||
deprecatedRegistryErrors = prometheus.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Subsystem: "registry",
|
||||
@ -96,6 +104,7 @@ func init() {
|
||||
prometheus.MustRegister(lastSyncTimestamp)
|
||||
prometheus.MustRegister(deprecatedRegistryErrors)
|
||||
prometheus.MustRegister(deprecatedSourceErrors)
|
||||
prometheus.MustRegister(controllerNoChangesTotal)
|
||||
}
|
||||
|
||||
// Controller is responsible for orchestrating the different components.
|
||||
@ -112,13 +121,15 @@ type Controller struct {
|
||||
// The interval between individual synchronizations
|
||||
Interval time.Duration
|
||||
// The DomainFilter defines which DNS records to keep or exclude
|
||||
DomainFilter endpoint.DomainFilter
|
||||
DomainFilter endpoint.DomainFilterInterface
|
||||
// The nextRunAt used for throttling and batching reconciliation
|
||||
nextRunAt time.Time
|
||||
// The nextRunAtMux is for atomic updating of nextRunAt
|
||||
nextRunAtMux sync.Mutex
|
||||
// DNS record types that will be considered for management
|
||||
ManagedRecordTypes []string
|
||||
// MinEventSyncInterval is used as window for batching events
|
||||
MinEventSyncInterval time.Duration
|
||||
}
|
||||
|
||||
// RunOnce runs a single iteration of a reconciliation loop.
|
||||
@ -147,32 +158,34 @@ func (c *Controller) RunOnce(ctx context.Context) error {
|
||||
Policies: []plan.Policy{c.Policy},
|
||||
Current: records,
|
||||
Desired: endpoints,
|
||||
DomainFilter: c.DomainFilter,
|
||||
DomainFilter: endpoint.MatchAllDomainFilters{c.DomainFilter, c.Registry.GetDomainFilter()},
|
||||
PropertyComparator: c.Registry.PropertyValuesEqual,
|
||||
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||
}
|
||||
|
||||
plan = plan.Calculate()
|
||||
|
||||
if plan.Changes.HasChanges() {
|
||||
err = c.Registry.ApplyChanges(ctx, plan.Changes)
|
||||
if err != nil {
|
||||
registryErrorsTotal.Inc()
|
||||
deprecatedRegistryErrors.Inc()
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
controllerNoChangesTotal.Inc()
|
||||
log.Info("All records are already up to date")
|
||||
}
|
||||
|
||||
lastSyncTimestamp.SetToCurrentTime()
|
||||
return nil
|
||||
}
|
||||
|
||||
// MinInterval is used as window for batching events
|
||||
const MinInterval = 5 * time.Second
|
||||
|
||||
// ScheduleRunOnce makes sure execution happens at most once per interval.
|
||||
func (c *Controller) ScheduleRunOnce(now time.Time) {
|
||||
c.nextRunAtMux.Lock()
|
||||
defer c.nextRunAtMux.Unlock()
|
||||
c.nextRunAt = now.Add(MinInterval)
|
||||
c.nextRunAt = now.Add(c.MinEventSyncInterval)
|
||||
}
|
||||
|
||||
func (c *Controller) ShouldRunOnce(now time.Time) bool {
|
||||
|
@ -40,6 +40,30 @@ type mockProvider struct {
|
||||
ExpectChanges *plan.Changes
|
||||
}
|
||||
|
||||
type filteredMockProvider struct {
|
||||
provider.BaseProvider
|
||||
domainFilter endpoint.DomainFilterInterface
|
||||
RecordsStore []*endpoint.Endpoint
|
||||
RecordsCallCount int
|
||||
ApplyChangesCalls []*plan.Changes
|
||||
}
|
||||
|
||||
func (p *filteredMockProvider) GetDomainFilter() endpoint.DomainFilterInterface {
|
||||
return p.domainFilter
|
||||
}
|
||||
|
||||
// Records returns the desired mock endpoints.
|
||||
func (p *filteredMockProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
|
||||
p.RecordsCallCount++
|
||||
return p.RecordsStore, nil
|
||||
}
|
||||
|
||||
// ApplyChanges stores all calls for later check
|
||||
func (p *filteredMockProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
|
||||
p.ApplyChangesCalls = append(p.ApplyChangesCalls, changes)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Records returns the desired mock endpoints.
|
||||
func (p *mockProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
|
||||
return p.RecordsStore, nil
|
||||
@ -155,7 +179,7 @@ func TestRunOnce(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestShouldRunOnce(t *testing.T) {
|
||||
ctrl := &Controller{Interval: 10 * time.Minute}
|
||||
ctrl := &Controller{Interval: 10 * time.Minute, MinEventSyncInterval: 5 * time.Second}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
@ -175,7 +199,7 @@ func TestShouldRunOnce(t *testing.T) {
|
||||
assert.False(t, ctrl.ShouldRunOnce(now.Add(100*time.Microsecond)))
|
||||
|
||||
// But after MinInterval we should run reconciliation
|
||||
now = now.Add(MinInterval)
|
||||
now = now.Add(5 * time.Second)
|
||||
assert.True(t, ctrl.ShouldRunOnce(now))
|
||||
|
||||
// But just one time
|
||||
@ -192,3 +216,155 @@ func TestShouldRunOnce(t *testing.T) {
|
||||
// But not two times
|
||||
assert.False(t, ctrl.ShouldRunOnce(now))
|
||||
}
|
||||
|
||||
func testControllerFiltersDomains(t *testing.T, configuredEndpoints []*endpoint.Endpoint, domainFilter endpoint.DomainFilterInterface, providerEndpoints []*endpoint.Endpoint, expectedChanges []*plan.Changes) {
|
||||
t.Helper()
|
||||
source := new(testutils.MockSource)
|
||||
source.On("Endpoints").Return(configuredEndpoints, nil)
|
||||
|
||||
// Fake some existing records in our DNS provider and validate some desired changes.
|
||||
provider := &filteredMockProvider{
|
||||
RecordsStore: providerEndpoints,
|
||||
}
|
||||
r, err := registry.NewNoopRegistry(provider)
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
ctrl := &Controller{
|
||||
Source: source,
|
||||
Registry: r,
|
||||
Policy: &plan.SyncPolicy{},
|
||||
DomainFilter: domainFilter,
|
||||
}
|
||||
|
||||
assert.NoError(t, ctrl.RunOnce(context.Background()))
|
||||
assert.Equal(t, 1, provider.RecordsCallCount)
|
||||
require.Len(t, provider.ApplyChangesCalls, len(expectedChanges))
|
||||
for i, change := range expectedChanges {
|
||||
assert.Equal(t, *change, *provider.ApplyChangesCalls[i])
|
||||
}
|
||||
}
|
||||
|
||||
func TestControllerSkipsEmptyChanges(t *testing.T) {
|
||||
testControllerFiltersDomains(
|
||||
t,
|
||||
[]*endpoint.Endpoint{
|
||||
{
|
||||
DNSName: "create-record.other.tld",
|
||||
RecordType: endpoint.RecordTypeA,
|
||||
Targets: endpoint.Targets{"1.2.3.4"},
|
||||
},
|
||||
{
|
||||
DNSName: "some-record.used.tld",
|
||||
RecordType: endpoint.RecordTypeA,
|
||||
Targets: endpoint.Targets{"8.8.8.8"},
|
||||
},
|
||||
},
|
||||
endpoint.NewDomainFilter([]string{"used.tld"}),
|
||||
[]*endpoint.Endpoint{
|
||||
{
|
||||
DNSName: "some-record.used.tld",
|
||||
RecordType: endpoint.RecordTypeA,
|
||||
Targets: endpoint.Targets{"8.8.8.8"},
|
||||
},
|
||||
},
|
||||
[]*plan.Changes{},
|
||||
)
|
||||
}
|
||||
|
||||
func TestWhenNoFilterControllerConsidersAllComain(t *testing.T) {
|
||||
testControllerFiltersDomains(
|
||||
t,
|
||||
[]*endpoint.Endpoint{
|
||||
{
|
||||
DNSName: "create-record.other.tld",
|
||||
RecordType: endpoint.RecordTypeA,
|
||||
Targets: endpoint.Targets{"1.2.3.4"},
|
||||
},
|
||||
{
|
||||
DNSName: "some-record.used.tld",
|
||||
RecordType: endpoint.RecordTypeA,
|
||||
Targets: endpoint.Targets{"8.8.8.8"},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
[]*endpoint.Endpoint{
|
||||
{
|
||||
DNSName: "some-record.used.tld",
|
||||
RecordType: endpoint.RecordTypeA,
|
||||
Targets: endpoint.Targets{"8.8.8.8"},
|
||||
},
|
||||
},
|
||||
[]*plan.Changes{
|
||||
{
|
||||
Create: []*endpoint.Endpoint{
|
||||
{
|
||||
DNSName: "create-record.other.tld",
|
||||
RecordType: endpoint.RecordTypeA,
|
||||
Targets: endpoint.Targets{"1.2.3.4"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestWhenMultipleControllerConsidersAllFilteredComain(t *testing.T) {
|
||||
testControllerFiltersDomains(
|
||||
t,
|
||||
[]*endpoint.Endpoint{
|
||||
{
|
||||
DNSName: "create-record.other.tld",
|
||||
RecordType: endpoint.RecordTypeA,
|
||||
Targets: endpoint.Targets{"1.2.3.4"},
|
||||
},
|
||||
{
|
||||
DNSName: "some-record.used.tld",
|
||||
RecordType: endpoint.RecordTypeA,
|
||||
Targets: endpoint.Targets{"1.1.1.1"},
|
||||
},
|
||||
{
|
||||
DNSName: "create-record.unused.tld",
|
||||
RecordType: endpoint.RecordTypeA,
|
||||
Targets: endpoint.Targets{"1.2.3.4"},
|
||||
},
|
||||
},
|
||||
endpoint.NewDomainFilter([]string{"used.tld", "other.tld"}),
|
||||
[]*endpoint.Endpoint{
|
||||
{
|
||||
DNSName: "some-record.used.tld",
|
||||
RecordType: endpoint.RecordTypeA,
|
||||
Targets: endpoint.Targets{"8.8.8.8"},
|
||||
},
|
||||
},
|
||||
[]*plan.Changes{
|
||||
{
|
||||
Create: []*endpoint.Endpoint{
|
||||
{
|
||||
DNSName: "create-record.other.tld",
|
||||
RecordType: endpoint.RecordTypeA,
|
||||
Targets: endpoint.Targets{"1.2.3.4"},
|
||||
},
|
||||
},
|
||||
UpdateOld: []*endpoint.Endpoint{
|
||||
{
|
||||
DNSName: "some-record.used.tld",
|
||||
RecordType: endpoint.RecordTypeA,
|
||||
Targets: endpoint.Targets{"8.8.8.8"},
|
||||
Labels: endpoint.Labels{},
|
||||
},
|
||||
},
|
||||
UpdateNew: []*endpoint.Endpoint{
|
||||
{
|
||||
DNSName: "some-record.used.tld",
|
||||
RecordType: endpoint.RecordTypeA,
|
||||
Targets: endpoint.Targets{"1.1.1.1"},
|
||||
Labels: endpoint.Labels{
|
||||
"owner": "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -1,39 +1,53 @@
|
||||
apiVersion: apiextensions.k8s.io/v1beta1
|
||||
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.5.0
|
||||
api-approved.kubernetes.io: "https://github.com/kubernetes-sigs/external-dns/pull/2007"
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
api: externaldns
|
||||
kubebuilder.k8s.io: 1.0.0
|
||||
name: dnsendpoints.externaldns.k8s.io
|
||||
spec:
|
||||
group: externaldns.k8s.io
|
||||
names:
|
||||
kind: DNSEndpoint
|
||||
listKind: DNSEndpointList
|
||||
plural: dnsendpoints
|
||||
singular: dnsendpoint
|
||||
scope: Namespaced
|
||||
subresources:
|
||||
status: {}
|
||||
validation:
|
||||
versions:
|
||||
- name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
properties:
|
||||
apiVersion:
|
||||
description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
|
||||
type: string
|
||||
kind:
|
||||
description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
description: DNSEndpointSpec defines the desired state of DNSEndpoint
|
||||
properties:
|
||||
endpoints:
|
||||
items:
|
||||
description: Endpoint is a high-level way of a connection between a service and an IP
|
||||
properties:
|
||||
dnsName:
|
||||
description: The hostname of the DNS record
|
||||
type: string
|
||||
labels:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: Labels stores labels defined for the Endpoint
|
||||
type: object
|
||||
providerSpecific:
|
||||
description: ProviderSpecific stores provider specific config
|
||||
items:
|
||||
description: ProviderSpecificProperty holds the name and value of a configuration which is specific to individual DNS providers
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
@ -42,11 +56,17 @@ spec:
|
||||
type: object
|
||||
type: array
|
||||
recordTTL:
|
||||
description: TTL for the record
|
||||
format: int64
|
||||
type: integer
|
||||
recordType:
|
||||
description: RecordType type of record, e.g. CNAME, A, SRV, TXT etc
|
||||
type: string
|
||||
setIdentifier:
|
||||
description: Identifier to distinguish multiple records with the same name and type (e.g. Route53 records with routing policies other than 'simple')
|
||||
type: string
|
||||
targets:
|
||||
description: The targets the DNS record points to
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
@ -54,9 +74,21 @@ spec:
|
||||
type: array
|
||||
type: object
|
||||
status:
|
||||
description: DNSEndpointStatus defines the observed state of DNSEndpoint
|
||||
properties:
|
||||
observedGeneration:
|
||||
description: The generation observed by the external-dns controller.
|
||||
format: int64
|
||||
type: integer
|
||||
type: object
|
||||
version: v1alpha1
|
||||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
status:
|
||||
acceptedNames:
|
||||
kind: ""
|
||||
plural: ""
|
||||
conditions: []
|
||||
storedVersions: []
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Quick Start
|
||||
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
- [Go 1.15+](https://golang.org/dl/)
|
||||
- [Go 1.16+](https://golang.org/dl/)
|
||||
- [Go modules](https://github.com/golang/go/wiki/Modules)
|
||||
- [golangci-lint](https://github.com/golangci/golangci-lint)
|
||||
- [Docker](https://docs.docker.com/install/)
|
||||
|
40
docs/faq.md
40
docs/faq.md
@ -28,24 +28,7 @@ ExternalDNS can solve this for you as well.
|
||||
|
||||
### Which DNS providers are supported?
|
||||
|
||||
Currently, the following providers are supported:
|
||||
|
||||
- Google Cloud DNS
|
||||
- AWS Route 53
|
||||
- AzureDNS
|
||||
- CloudFlare
|
||||
- DigitalOcean
|
||||
- DNSimple
|
||||
- Infoblox
|
||||
- Dyn
|
||||
- OpenStack Designate
|
||||
- PowerDNS
|
||||
- CoreDNS
|
||||
- Exoscale
|
||||
- Oracle Cloud Infrastructure DNS
|
||||
- Linode DNS
|
||||
- RFC2136
|
||||
- TransIP
|
||||
Please check the [provider status table](https://github.com/kubernetes-sigs/external-dns#status-of-providers) for the list of supported providers and their status.
|
||||
|
||||
As stated in the README, we are currently looking for stable maintainers for those providers, to ensure that bugfixes and new features will be available for all of those.
|
||||
|
||||
@ -57,7 +40,9 @@ Services exposed via `type=LoadBalancer`, `type=ExternalName` and for the hostna
|
||||
|
||||
There are three sources of information for ExternalDNS to decide on DNS name. ExternalDNS will pick one in order as listed below:
|
||||
|
||||
1. For ingress objects ExternalDNS will create a DNS record based on the host specified for the ingress object. For services ExternalDNS will look for the annotation `external-dns.alpha.kubernetes.io/hostname` on the service and use the loadbalancer IP, it also will look for the annotation `external-dns.alpha.kubernetes.io/internal-hostname` on the service and use the service IP.
|
||||
1. For ingress objects ExternalDNS will create a DNS record based on the hosts specified for the ingress object, as well as the `external-dns.alpha.kubernetes.io/hostname` annotation. For services ExternalDNS will look for the annotation `external-dns.alpha.kubernetes.io/hostname` on the service and use the loadbalancer IP, it also will look for the annotation `external-dns.alpha.kubernetes.io/internal-hostname` on the service and use the service IP.
|
||||
- For ingresses, you can optionally force ExternalDNS to create records based on _either_ the hosts specified or the `external-dns.alpha.kubernetes.io/hostname` annotation. This behavior is controlled by
|
||||
setting the `external-dns.alpha.kubernetes.io/ingress-hostname-source` annotation on that ingress to either `defined-hosts-only` or `annotation-only`.
|
||||
|
||||
2. If compatibility mode is enabled (e.g. `--compatibility={mate,molecule}` flag), External DNS will parse annotations used by Zalando/Mate, wearemolecule/route53-kubernetes. Compatibility mode with Kops DNS Controller is planned to be added in the future.
|
||||
|
||||
@ -75,7 +60,9 @@ Regarding Ingress, we'll support:
|
||||
* Google's Ingress Controller on GKE that integrates with their Layer 7 load balancers (GLBC)
|
||||
* nginx-ingress-controller v0.9.x with a fronting Service
|
||||
* Zalando's [AWS Ingress controller](https://github.com/zalando-incubator/kube-ingress-aws-controller), based on AWS ALBs and [Skipper](https://github.com/zalando/skipper)
|
||||
* [Traefik](https://github.com/containous/traefik) 1.7 and above, when [`kubernetes.ingressEndpoint`](https://docs.traefik.io/v1.7/configuration/backends/kubernetes/#ingressendpoint) is configured (`kubernetes.ingressEndpoint.useDefaultPublishedService` in the [Helm chart](https://github.com/helm/charts/tree/HEAD/stable/traefik#configuration))
|
||||
* [Traefik](https://github.com/containous/traefik)
|
||||
* version 1.7, when [`kubernetes.ingressEndpoint`](https://docs.traefik.io/v1.7/configuration/backends/kubernetes/#ingressendpoint) is configured (`kubernetes.ingressEndpoint.useDefaultPublishedService` in the [Helm chart](https://github.com/helm/charts/tree/HEAD/stable/traefik#configuration))
|
||||
* versions \>=2.0, when [`providers.kubernetesIngress.ingressEndpoint`](https://doc.traefik.io/traefik/providers/kubernetes-ingress/#ingressendpoint) is configured (`providers.kubernetesIngress.publishedService.enabled` is set to `true` in the [new Helm chart](https://github.com/traefik/traefik-helm-chart))
|
||||
|
||||
### Are other Ingress Controllers supported?
|
||||
|
||||
@ -192,7 +179,7 @@ You can use the host label in the metric to figure out if the request was agains
|
||||
Here is the full list of available metrics provided by ExternalDNS:
|
||||
|
||||
| Name | Description | Type |
|
||||
|-----------------------------------------------------|---------------------------------------------------------|---------|
|
||||
| --------------------------------------------------- | ------------------------------------------------------- | ------- |
|
||||
| external_dns_controller_last_sync_timestamp_seconds | Timestamp of last successful sync with the DNS provider | Gauge |
|
||||
| external_dns_registry_endpoints_total | Number of Endpoints in all sources | Gauge |
|
||||
| external_dns_registry_errors_total | Number of Registry errors | Counter |
|
||||
@ -214,7 +201,7 @@ $ docker run \
|
||||
-e EXTERNAL_DNS_SOURCE=$'service\ningress' \
|
||||
-e EXTERNAL_DNS_PROVIDER=google \
|
||||
-e EXTERNAL_DNS_DOMAIN_FILTER=$'foo.com\nbar.com' \
|
||||
k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
time="2017-08-08T14:10:26Z" level=info msg="config: &{APIServerURL: KubeConfig: Sources:[service ingress] Namespace: ...
|
||||
```
|
||||
|
||||
@ -269,8 +256,11 @@ one to expose DNS to the internet.
|
||||
|
||||
To do this with ExternalDNS you can use the `--annotation-filter` to specifically tie an instance of ExternalDNS to
|
||||
an instance of a ingress controller. Let's assume you have two ingress controllers `nginx-internal` and `nginx-external`
|
||||
then you can start two ExternalDNS providers one with `--annotation-filter=kubernetes.io/ingress.class=nginx-internal`
|
||||
and one with `--annotation-filter=kubernetes.io/ingress.class=nginx-external`.
|
||||
then you can start two ExternalDNS providers one with `--annotation-filter=kubernetes.io/ingress.class in (nginx-internal)`
|
||||
and one with `--annotation-filter=kubernetes.io/ingress.class in (nginx-external)`.
|
||||
|
||||
If you need to search for multiple values of said annotation, you can provide a comma separated list, like so:
|
||||
`--annotation-filter=kubernetes.io/ingress.class in (nginx-internal, alb-ingress-internal)`.
|
||||
|
||||
Beware when using multiple sources, e.g. `--source=service --source=ingress`, `--annotation-filter` will filter every given source objects.
|
||||
If you need to filter only one specific source you have to run a separated external dns service containing only the wanted `--source` and `--annotation-filter`.
|
||||
@ -302,7 +292,7 @@ When we tag a new release, we push a container image to the Kubernetes projects
|
||||
k8s.gcr.io/external-dns/external-dns
|
||||
```
|
||||
|
||||
As tags, you use the external-dns release of choice(i.e. `v0.7.3`). A `latest` tag is not provided in the container registry.
|
||||
As tags, you use the external-dns release of choice(i.e. `v0.7.6`). A `latest` tag is not provided in the container registry.
|
||||
|
||||
If you wish to build your own image, you can use the provided [Dockerfile](../Dockerfile) as a starting point.
|
||||
|
||||
|
@ -1,84 +0,0 @@
|
||||
# Kops dns-controller annotations
|
||||
|
||||
Kops includes a dns-controller, and this document describes the existing annotations and their behaviour. This
|
||||
document is intended to allow us to see the use-cases identified by kops dns-controller, to ensure the same annotations
|
||||
can be recognized (perhaps with a `--compatibilty` flag), and to ensure that we have comparable functionality.
|
||||
|
||||
## Flags
|
||||
|
||||
* `--dns`: `aws-route53,google-clouddns`
|
||||
|
||||
The DNS flag lets us choose which DNS provider to use.
|
||||
|
||||
* `--watch-ingress` boolean
|
||||
|
||||
Turns ingress functionality on and off. For AWS at least, we are blocked on switching to a release
|
||||
from the `kubernetes/ingress` project (instead of one from the `contrib` project).
|
||||
|
||||
* `--zones` configures permitted zones, and also disambiguates when domain names are duplicated. It is a list that matches zones we are allowed to match.
|
||||
|
||||
- `*` and `*/*` are wildcard, and match all zones
|
||||
|
||||
- `example.com` matches zones with name=`example.com`
|
||||
|
||||
- `example.com/1234` matches zones with id=`1234` and name=`example.com`. This is useful to disambiguate between
|
||||
multiple zones named `example.com`.
|
||||
|
||||
- `*/1234` matches the zone with id=`1234`. A zone has a unique name, so this is equivalent to `example.com/1234`,
|
||||
but a little shorter - and less self-documenting!
|
||||
|
||||
* Standard glog flags (--v, --logtostderr etc)
|
||||
|
||||
* Standard kubectl_util client flags
|
||||
|
||||
|
||||
## Annotations
|
||||
|
||||
We define 2 primary annotations:
|
||||
|
||||
* `dns.alpha.kubernetes.io/external` which is used to define a DNS record for accessing the resource publicly (i.e. public IPs)
|
||||
|
||||
* `dns.alpha.kubernetes.io/internal` which is used to define a DNS record for accessing the resource from outside the cluster but inside the cloud,
|
||||
i.e. it will typically use internal IPs for instances.
|
||||
|
||||
These annotations may both be comma-separated lists of names.
|
||||
|
||||
On a node, we also have a WIP annotation `dns.alpha.kubernetes.io/external-ip`, which configures the external ip
|
||||
for a node (to work around [#42125](https://github.com/kubernetes/kubernetes/issues/42125)). That is an annotation
|
||||
that lets us defined the equivalent of an address with type ExternalIP.
|
||||
|
||||
## DNS record mappings
|
||||
|
||||
The DNS record mappings try to "do the right thing", but what this means is different for each resource type.
|
||||
|
||||
### Ingress
|
||||
|
||||
We consult the `Status.LoadBalancer.Ingress` records on the ingress. For each one, we create a record.
|
||||
If the record is an IP address, we add an A record. If the record is a hostname (AWS ELB), we use a CNAME.
|
||||
|
||||
We would like to use an ALIAS, but we have not yet done this because of limitations of the DNS provider.
|
||||
|
||||
### Pods
|
||||
|
||||
For the external annotation, we will map a HostNetwork=true pod to the external IPs of the node. We create an A record.
|
||||
|
||||
For the internal annotation, we will map a HostNetwork=true pod to the internal IPs of the node. We create an A record.
|
||||
|
||||
We ignore pods that are not HostNetwork=true
|
||||
|
||||
### Services
|
||||
|
||||
* For a Service of Type=LoadBalancer, we look at Status.LoadBalancer.Ingress. We create CNAMEs to hostnames,
|
||||
and A records for IP addresses. (We should create ALIASes for ELBs). We do this for both internal & external
|
||||
names - there is no difference on GCE or AWS.
|
||||
|
||||
* For a Service of Type=NodePort, we create A records for the node's internal/external IP addresses, as appropriate.
|
||||
|
||||
(A canonical use for NodePort internal is having a prometheus server running inside EC2 monitoring your kubernetes cluster,
|
||||
for NodePort external is to expose your service without an ELB).
|
||||
|
||||
|
||||
### Nodes
|
||||
|
||||
(We don't currently support annotations on the nodes themselves. We do set up internal "alias" records,
|
||||
which is how we do JOINs for e.g. NodePort services)
|
@ -4,6 +4,16 @@
|
||||
|
||||
Currently we don't release regularly. Whenever we think it makes sense to release a new version we do it, but we aim to do a new release every month. You might want to ask in our Slack channel [external-dns](https://kubernetes.slack.com/archives/C771MKDKQ) when the next release will come out.
|
||||
|
||||
## Versioning convention
|
||||
|
||||
These are the conventions that we will be using for releases following `0.7.6`:
|
||||
|
||||
- **Patch** version should be updated if we need to merge bugfixes, e.g. provider a does need a fix in order make updates working again. I would see updating or improving documentation here.
|
||||
|
||||
- **Minor** version should be updated if new features are implemented in existing providers or new provider get introduced.
|
||||
|
||||
- **Major** version should be upgraded if we introduce breaking changes.
|
||||
|
||||
## How to release a new image
|
||||
|
||||
### Prerequisite
|
||||
|
@ -2,17 +2,17 @@
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Akamai Edge DNS (formally known as Fast DNS) provider support was first released in External-DNS v0.5.18
|
||||
External-DNS v0.8.0 or greater.
|
||||
|
||||
### Zones
|
||||
|
||||
External-DNS manages service endpoints in existing DNS zones. The Akamai provider does not add, remove or configure new zones in anyway. Edge DNS zones can be created and managed thru the [Akamai Control Center](https://control.akamai.com) or [Akamai DevOps Tools](https://developer.akamai.com/devops), [Akamai CLI](https://developer.akamai.com/cli) and [Akamai Terraform Provider](https://developer.akamai.com/tools/integrations/terraform)
|
||||
External-DNS manages service endpoints in existing DNS zones. The Akamai provider does not add, remove or configure new zones. The [Akamai Control Center](https://control.akamai.com) or [Akamai DevOps Tools](https://developer.akamai.com/devops), [Akamai CLI](https://developer.akamai.com/cli) and [Akamai Terraform Provider](https://developer.akamai.com/tools/integrations/terraform) can create and manage Edge DNS zones.
|
||||
|
||||
### Akamai Edge DNS Authentication
|
||||
|
||||
The Akamai Edge DNS provider requires valid Akamai Edgegrid API authentication credentials to access zones and manage associated DNS records.
|
||||
The Akamai Edge DNS provider requires valid Akamai Edgegrid API authentication credentials to access zones and manage DNS records.
|
||||
|
||||
Credentials can be provided to the provider either directly by key or indirectly via a file. The Akamai credential keys and mappings to the Akamai provider utilizing different presentation methods are:
|
||||
Either directly by key or indirectly via a file can set credentials for the provider. The Akamai credential keys and mappings to the Akamai provider utilizing different presentation methods are:
|
||||
|
||||
| Edgegrid Auth Key | External-DNS Cmd Line Key | Environment/ConfigMap Key | Description |
|
||||
| ----------------- | ------------------------- | ------------------------- | ----------- |
|
||||
@ -21,25 +21,20 @@ Credentials can be provided to the provider either directly by key or indirectly
|
||||
| client_token | akamai-client-token | EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN |Akamai Edgegrid API client token |
|
||||
| client-secret | akamai-client-secret | EXTERNAL_DNS_AKAMAI_CLIENT_SECRET |Akamai Edgegrid API client secret |
|
||||
|
||||
In addition to specifying auth credentials individually, the credentials may be referenced indirectly by using the Akamai Edgegrid .edgerc file convention.
|
||||
In addition to specifying auth credentials individually, an Akamai Edgegrid .edgerc file convention can set credentials.
|
||||
|
||||
| External-DNS Cmd Line | Environment/ConfigMap | Description |
|
||||
| --------------------- | --------------------- | ----------- |
|
||||
| akamai-edgerc-path | EXTERNAL_DNS_AKAMAI_EDGERC_PATH | Accessible path to Edgegrid credentials file, e.g /home/test/.edgerc |
|
||||
| akamai-edgerc-section | EXTERNAL_DNS_AKAMAI_EDGERC_SECTION | Section in Edgegrid credentials file containing credentials |
|
||||
|
||||
Note: akamai-edgerc-path and akamai-edgerc-section are present in External-DNS versions after v0.7.5
|
||||
|
||||
[Akamai API Authentication](https://developer.akamai.com/getting-started/edgegrid) provides an overview and further information pertaining to the generation of auth credentials for API base applications and tools.
|
||||
|
||||
The following example defines and references a Kubernetes ConfigMap secret, applied by referencing the secret and its keys in the env section of the deployment.
|
||||
|
||||
[Akamai API Authentication](https://developer.akamai.com/getting-started/edgegrid) provides an overview and further information about authorization credentials for API base applications and tools.
|
||||
|
||||
## Deploy External-DNS
|
||||
|
||||
An operational External-DNS deployment consists of an External-DNS container and service. The following sections demonstrate the ConfigMap objects that would make up an example functional external DNS kubernetes configuration utilizing NGINX as the exposed service.
|
||||
An operational External-DNS deployment consists of an External-DNS container and service. The following sections demonstrate the ConfigMap objects that would make up an example functional external DNS kubernetes configuration utilizing NGINX as the service.
|
||||
|
||||
Connect your `kubectl` client to the cluster with which you want to test External-DNS, and then apply one of the following manifest files for deployment:
|
||||
Connect your `kubectl` client to the External-DNS cluster, and then apply one of the following manifest files:
|
||||
|
||||
### Manifest (for clusters without RBAC enabled)
|
||||
|
||||
@ -59,9 +54,10 @@ spec:
|
||||
labels:
|
||||
app: external-dns
|
||||
spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.5
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.8.0
|
||||
args:
|
||||
- --source=service # or ingress or both
|
||||
- --provider=akamai
|
||||
@ -69,6 +65,7 @@ spec:
|
||||
# zone-id-filter may be specified as well to filter on contract ID
|
||||
- --registry=txt
|
||||
- --txt-owner-id={{ owner-id-for-this-external-dns }}
|
||||
- --txt-prefix={{ prefix label for TXT record }}.
|
||||
env:
|
||||
- name: EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN
|
||||
valueFrom:
|
||||
@ -143,9 +140,10 @@ spec:
|
||||
labels:
|
||||
app: external-dns
|
||||
spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.5
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.8.0
|
||||
args:
|
||||
- --source=service # or ingress or both
|
||||
- --provider=akamai
|
||||
@ -153,6 +151,7 @@ spec:
|
||||
# zone-id-filter may be specified as well to filter on contract ID
|
||||
- --registry=txt
|
||||
- --txt-owner-id={{ owner-id-for-this-external-dns }}
|
||||
- --txt-prefix={{ prefix label for TXT record }}.
|
||||
env:
|
||||
- name: EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN
|
||||
valueFrom:
|
||||
@ -179,7 +178,7 @@ spec:
|
||||
Create the deployment for External-DNS:
|
||||
|
||||
```
|
||||
$ kubectl create -f externaldns.yaml
|
||||
$ kubectl apply -f externaldns.yaml
|
||||
```
|
||||
|
||||
## Deploying an Nginx Service
|
||||
@ -223,21 +222,21 @@ spec:
|
||||
targetPort: 80
|
||||
```
|
||||
|
||||
Create the deployment, service and ingress object:
|
||||
Create the deployment and service object:
|
||||
|
||||
```
|
||||
$ kubectl create -f nginx.yaml
|
||||
$ kubectl apply -f nginx.yaml
|
||||
```
|
||||
|
||||
## Verify Akamai Edge DNS Records
|
||||
|
||||
It is recommended to wait 3-5 minutes before validating the records to allow the record changes to propagate to all the Akamai name servers worldwide.
|
||||
Wait 3-5 minutes before validating the records to allow the record changes to propagate to all the Akamai name servers.
|
||||
|
||||
The records can be validated using the [Akamai Control Center](http://control.akamai.com) or by executing a dig, nslookup or similar DNS command.
|
||||
Validate records using the [Akamai Control Center](http://control.akamai.com) or by executing a dig, nslookup or similar DNS command.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Once you successfully configure and verify record management via External-DNS, you can delete the tutorial's example:
|
||||
Once you successfully configure and verify record management via External-DNS, you can delete the tutorial's examples:
|
||||
|
||||
```
|
||||
$ kubectl delete -f nginx.yaml
|
||||
@ -246,6 +245,5 @@ $ kubectl delete -f externaldns.yaml
|
||||
|
||||
## Additional Information
|
||||
|
||||
* The Akamai provider allows the administrative user to filter zones by both name (domain-filter) and contract Id (zone-id-filter). The Edge DNS API will return a '500 Internal Error' if an invalid contract Id is provided.
|
||||
* The provider will substitute any embedded quotes in TXT records with `` ` `` (back tick) when writing the records to the API.
|
||||
|
||||
* The Akamai provider allows the administrative user to filter zones by both name (`domain-filter`) and contract Id (`zone-id-filter`). The Edge DNS API will return a '500 Internal Error' for invalid contract Ids.
|
||||
* The provider will substitute quotes in TXT records with a `` ` `` (back tick) when writing records with the API.
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
This tutorial describes how to use ExternalDNS with the [aws-alb-ingress-controller][1].
|
||||
|
||||
[1]: https://kubernetes-sigs.github.io/aws-alb-ingress-controller/
|
||||
[1]: https://kubernetes-sigs.github.io/aws-load-balancer-controller
|
||||
|
||||
## Setting up ExternalDNS and aws-alb-ingress-controller
|
||||
|
||||
@ -14,12 +14,12 @@ this is not required.
|
||||
|
||||
For help setting up the ALB Ingress Controller, follow the [Setup Guide][2].
|
||||
|
||||
[2]: https://kubernetes-sigs.github.io/aws-alb-ingress-controller/guide/controller/setup/
|
||||
[2]: https://kubernetes-sigs.github.io/aws-load-balancer-controller/latest/deploy/installation/
|
||||
|
||||
Note that the ALB ingress controller uses the same tags for [subnet auto-discovery][3]
|
||||
as Kubernetes does with the AWS cloud provider.
|
||||
|
||||
[3]: https://kubernetes-sigs.github.io/aws-alb-ingress-controller/guide/controller/config/#subnet-auto-discovery
|
||||
[3]: https://kubernetes-sigs.github.io/aws-load-balancer-controller/latest/deploy/subnet_discovery/
|
||||
|
||||
In the examples that follow, it is assumed that you configured the ALB Ingress
|
||||
Controller with the `ingress-class=alb` argument (not to be confused with the
|
||||
@ -75,7 +75,7 @@ type `LoadBalancer` here, since we will be using an Ingress to create an ALB.
|
||||
Create the following Ingress to expose the echoserver application to the Internet.
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
@ -110,7 +110,7 @@ this Ingress object will only be fronting one backend Service, we might instead
|
||||
create the following:
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
@ -145,7 +145,7 @@ and one AAAA record) for each hostname associated with the Ingress object.
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
|
@ -113,7 +113,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
args:
|
||||
- --source=service
|
||||
- --source=ingress
|
||||
@ -141,7 +141,7 @@ kind: ServiceAccount
|
||||
metadata:
|
||||
name: external-dns
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: external-dns
|
||||
@ -156,7 +156,7 @@ rules:
|
||||
resources: ["nodes"]
|
||||
verbs: ["list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: external-dns-viewer
|
||||
@ -187,7 +187,7 @@ spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
args:
|
||||
- --source=service
|
||||
- --source=ingress
|
||||
@ -229,7 +229,7 @@ Create an ingress resource manifest file.
|
||||
> For ingress objects ExternalDNS will create a DNS record based on the host specified for the ingress object.
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: foo
|
||||
|
@ -81,7 +81,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
env:
|
||||
- name: AWS_REGION
|
||||
value: us-east-1 # put your CloudMap NameSpace region
|
||||
@ -102,7 +102,7 @@ kind: ServiceAccount
|
||||
metadata:
|
||||
name: external-dns
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: external-dns
|
||||
@ -117,7 +117,7 @@ rules:
|
||||
resources: ["nodes"]
|
||||
verbs: ["list","watch"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: external-dns-viewer
|
||||
@ -148,7 +148,7 @@ spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
env:
|
||||
- name: AWS_REGION
|
||||
value: us-east-1 # put your CloudMap NameSpace region
|
||||
|
@ -141,7 +141,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
args:
|
||||
- --source=service
|
||||
- --source=ingress
|
||||
@ -166,7 +166,7 @@ metadata:
|
||||
# Substitute your account ID and IAM service role name below.
|
||||
eks.amazonaws.com/role-arn: arn:aws:iam::ACCOUNT-ID:role/IAM-SERVICE-ROLE-NAME
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: external-dns
|
||||
@ -181,7 +181,7 @@ rules:
|
||||
resources: ["nodes"]
|
||||
verbs: ["list","watch"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: external-dns-viewer
|
||||
@ -216,7 +216,7 @@ spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
args:
|
||||
- --source=service
|
||||
- --source=ingress
|
||||
@ -253,7 +253,7 @@ Create an ingress resource manifest file.
|
||||
> For ingress objects ExternalDNS will create a DNS record based on the host specified for the ingress object.
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: foo
|
||||
|
@ -171,7 +171,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: externaldns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.8.0
|
||||
args:
|
||||
- --source=service
|
||||
- --source=ingress
|
||||
@ -196,7 +196,7 @@ kind: ServiceAccount
|
||||
metadata:
|
||||
name: externaldns
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: externaldns
|
||||
@ -211,7 +211,7 @@ rules:
|
||||
resources: ["nodes"]
|
||||
verbs: ["get", "watch", "list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: externaldns-viewer
|
||||
@ -242,7 +242,7 @@ spec:
|
||||
serviceAccountName: externaldns
|
||||
containers:
|
||||
- name: externaldns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.8.0
|
||||
args:
|
||||
- --source=service
|
||||
- --source=ingress
|
||||
@ -271,7 +271,7 @@ kind: ServiceAccount
|
||||
metadata:
|
||||
name: externaldns
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: externaldns
|
||||
@ -283,7 +283,7 @@ rules:
|
||||
resources: ["ingresses"]
|
||||
verbs: ["get","watch","list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: externaldns
|
||||
@ -313,7 +313,7 @@ spec:
|
||||
serviceAccountName: externaldns
|
||||
containers:
|
||||
- name: externaldns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.8.0
|
||||
args:
|
||||
- --source=service
|
||||
- --source=ingress
|
||||
@ -375,7 +375,7 @@ spec:
|
||||
type: ClusterIP
|
||||
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: nginx
|
||||
|
@ -61,7 +61,7 @@ The `resourceGroup` is the Resource Group created in a previous step.
|
||||
The `aadClientID` and `aaClientSecret` are associated with the Service Principal, that you need to create next.
|
||||
|
||||
### Creating service principal
|
||||
A Service Principal with a minimum access level of `contributor` to the DNS zone(s) and `reader` to the resource group containing the Azure DNS zone(s) is necessary for ExternalDNS to be able to edit DNS records. However, other more permissive access levels will work too (e.g. `contributor` to the resource group or the whole subscription).
|
||||
A Service Principal with a minimum access level of `DNS Zone Contributor` or `Contributor` to the DNS zone(s) and `Reader` to the resource group containing the Azure DNS zone(s) is necessary for ExternalDNS to be able to edit DNS records. However, other more permissive access levels will work too (e.g. `Contributor` to the resource group or the whole subscription).
|
||||
|
||||
This is an Azure CLI example on how to query the Azure API for the information required for the Resource Group and DNS zone you would have already created in previous steps.
|
||||
|
||||
@ -136,7 +136,7 @@ $ kubectl create secret generic azure-config-file --from-file=/local/path/to/azu
|
||||
|
||||
### Azure Managed Service Identity (MSI)
|
||||
|
||||
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.
|
||||
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. Note that when granting access the kubeletidentity must be used, not the MSI used for the cluster (it usually has a name in the format <Clustername>-<agentpool>).
|
||||
|
||||
The contents of `azure.json` should be similar to this:
|
||||
|
||||
@ -191,7 +191,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.8.0
|
||||
args:
|
||||
- --source=service
|
||||
- --source=ingress
|
||||
@ -206,6 +206,9 @@ spec:
|
||||
- name: azure-config-file
|
||||
secret:
|
||||
secretName: azure-config-file
|
||||
items:
|
||||
- key: externaldns-config.json
|
||||
path: azure.json
|
||||
```
|
||||
|
||||
### Manifest (for clusters with RBAC enabled, cluster access)
|
||||
@ -215,7 +218,7 @@ kind: ServiceAccount
|
||||
metadata:
|
||||
name: external-dns
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: external-dns
|
||||
@ -230,7 +233,7 @@ rules:
|
||||
resources: ["nodes"]
|
||||
verbs: ["list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: external-dns-viewer
|
||||
@ -261,13 +264,14 @@ spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.8.0
|
||||
args:
|
||||
- --source=service
|
||||
- --source=ingress
|
||||
- --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
|
||||
- --provider=azure
|
||||
- --azure-resource-group=externaldns # (optional) use the DNS zones from the tutorial's resource group
|
||||
- --txt-prefix=externaldns-
|
||||
volumeMounts:
|
||||
- name: azure-config-file
|
||||
mountPath: /etc/kubernetes
|
||||
@ -276,6 +280,9 @@ spec:
|
||||
- name: azure-config-file
|
||||
secret:
|
||||
secretName: azure-config-file
|
||||
items:
|
||||
- key: externaldns-config.json
|
||||
path: azure.json
|
||||
```
|
||||
|
||||
### Manifest (for clusters with RBAC enabled, namespace access)
|
||||
@ -289,7 +296,7 @@ kind: ServiceAccount
|
||||
metadata:
|
||||
name: external-dns
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: external-dns
|
||||
@ -301,7 +308,7 @@ rules:
|
||||
resources: ["ingresses"]
|
||||
verbs: ["get","watch","list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: external-dns
|
||||
@ -331,7 +338,7 @@ spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.8.0
|
||||
args:
|
||||
- --source=service
|
||||
- --source=ingress
|
||||
@ -346,6 +353,9 @@ spec:
|
||||
- name: azure-config-file
|
||||
secret:
|
||||
secretName: azure-config-file
|
||||
items:
|
||||
- key: externaldns-config.json
|
||||
path: azure.json
|
||||
```
|
||||
|
||||
Create the deployment for ExternalDNS:
|
||||
@ -392,7 +402,7 @@ spec:
|
||||
type: ClusterIP
|
||||
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: nginx
|
||||
|
85
docs/tutorials/bluecat.md
Normal file
85
docs/tutorials/bluecat.md
Normal file
@ -0,0 +1,85 @@
|
||||
# Setting up external-dns for BlueCat
|
||||
|
||||
The first external-dns release with with BlueCat provider support is v0.8.0.
|
||||
|
||||
## Prerequisites
|
||||
Install the BlueCat Gateway product and deploy the [community gateway workflows](https://github.com/bluecatlabs/gateway-workflows).
|
||||
|
||||
## Configuration Options
|
||||
|
||||
The options for configuring the Bluecat Provider are available through the json file provided to External-DNS via the flag `--bluecat-config-file`. The
|
||||
BlueCat Gateway username and password can be supplied using the configuration file or environment variables `BLUECAT_USERNAME` and `BLUECAT_PASSWORD`.
|
||||
|
||||
| Key | Required |
|
||||
| ----------------- | ------------------ |
|
||||
| gatewayHost | Yes |
|
||||
| gatewayUsername | No |
|
||||
| gatewayPassword | No |
|
||||
| dnsConfiguration | Yes |
|
||||
| dnsView | Yes |
|
||||
| rootZone | Yes |
|
||||
| skipTLSVerify | No (default false) |
|
||||
|
||||
## 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",
|
||||
"skipTLSVerify": false
|
||||
}
|
||||
EOF
|
||||
kubectl create secret generic bluecatconfig --from-file ~/bluecat.json -n bluecat-example
|
||||
```
|
||||
|
||||
Setup up namespace, deployment, and service account:
|
||||
```
|
||||
kubectl create namespace bluecat-example
|
||||
cat << EOF > ~/bluecat.yml
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: external-dns
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: external-dns
|
||||
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:v0.8.0
|
||||
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
|
||||
EOF
|
||||
kubectl apply -f ~/bluecat.yml -n bluecat-example
|
||||
```
|
@ -50,7 +50,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
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.
|
||||
@ -72,7 +72,7 @@ kind: ServiceAccount
|
||||
metadata:
|
||||
name: external-dns
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: external-dns
|
||||
@ -87,7 +87,7 @@ rules:
|
||||
resources: ["nodes"]
|
||||
verbs: ["list", "watch"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: external-dns-viewer
|
||||
@ -118,7 +118,7 @@ spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
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.
|
||||
|
@ -26,7 +26,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
args:
|
||||
- --source=service
|
||||
- --source=ingress
|
||||
@ -48,7 +48,7 @@ kind: ServiceAccount
|
||||
metadata:
|
||||
name: external-dns
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: external-dns
|
||||
@ -71,7 +71,7 @@ rules:
|
||||
resources: ["httpproxies"]
|
||||
verbs: ["get","watch","list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: external-dns-viewer
|
||||
@ -102,7 +102,7 @@ spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
args:
|
||||
- --source=service
|
||||
- --source=ingress
|
||||
|
@ -108,7 +108,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
args:
|
||||
- --source=ingress
|
||||
- --provider=coredns
|
||||
@ -122,7 +122,7 @@ spec:
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: external-dns
|
||||
@ -137,7 +137,7 @@ rules:
|
||||
resources: ["nodes"]
|
||||
verbs: ["list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: external-dns-viewer
|
||||
@ -175,7 +175,7 @@ spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
args:
|
||||
- --source=ingress
|
||||
- --provider=coredns
|
||||
@ -194,7 +194,7 @@ minikube addons enable ingress
|
||||
## Testing ingress example
|
||||
```
|
||||
$ cat ingress.yaml
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: nginx
|
||||
|
@ -59,7 +59,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
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.
|
||||
@ -87,7 +87,7 @@ kind: ServiceAccount
|
||||
metadata:
|
||||
name: external-dns
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: external-dns
|
||||
@ -105,7 +105,7 @@ rules:
|
||||
resources: ["nodes"]
|
||||
verbs: ["watch","list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: external-dns-viewer
|
||||
@ -136,7 +136,7 @@ spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
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.
|
||||
|
@ -43,7 +43,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
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.
|
||||
@ -60,7 +60,7 @@ kind: ServiceAccount
|
||||
metadata:
|
||||
name: external-dns
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: external-dns
|
||||
@ -75,7 +75,7 @@ rules:
|
||||
resources: ["nodes"]
|
||||
verbs: ["list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: external-dns-viewer
|
||||
@ -107,7 +107,7 @@ spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
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.
|
||||
|
@ -35,7 +35,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
args:
|
||||
- --source=service
|
||||
- --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone you create in DNSimple.
|
||||
@ -54,7 +54,7 @@ kind: ServiceAccount
|
||||
metadata:
|
||||
name: external-dns
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: external-dns
|
||||
@ -69,7 +69,7 @@ rules:
|
||||
resources: ["nodes"]
|
||||
verbs: ["list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: external-dns-viewer
|
||||
@ -100,7 +100,7 @@ spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
args:
|
||||
- --source=service
|
||||
- --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone you create in DNSimple.
|
||||
|
@ -43,7 +43,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
args:
|
||||
- --source=ingress
|
||||
- --txt-prefix=_d
|
||||
@ -111,7 +111,7 @@ Having `--dry-run=true` and `--log-level=debug` is a great way to see _exactly_
|
||||
Create a file called 'test-ingress.yaml' with the following contents:
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: test-ingress
|
||||
|
@ -41,7 +41,7 @@ spec:
|
||||
# serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
args:
|
||||
- --source=ingress # or service or both
|
||||
- --provider=exoscale
|
||||
@ -66,7 +66,7 @@ metadata:
|
||||
|
||||
---
|
||||
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: external-dns
|
||||
@ -83,7 +83,7 @@ rules:
|
||||
|
||||
---
|
||||
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: external-dns-viewer
|
||||
@ -104,7 +104,7 @@ subjects:
|
||||
Spin up a simple nginx HTTP server with the following spec (`kubectl apply -f`):
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: nginx
|
||||
|
@ -27,7 +27,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
args:
|
||||
- --log-level=debug
|
||||
- --source=service
|
||||
|
191
docs/tutorials/gandi.md
Normal file
191
docs/tutorials/gandi.md
Normal file
@ -0,0 +1,191 @@
|
||||
# Setting up ExternalDNS for Services on Gandi
|
||||
|
||||
This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using Gandi.
|
||||
|
||||
Make sure to use **>=0.7.7** version of ExternalDNS for this tutorial.
|
||||
|
||||
## Creating a Gandi DNS zone (domain)
|
||||
|
||||
Create a new DNS zone where you want to create your records in. Let's use `example.com` as an example here. Make sure the zone uses
|
||||
|
||||
## Creating Gandi API Key
|
||||
|
||||
Generate an API key on [your account](https://account.gandi.net) (click on "Security").
|
||||
|
||||
The environment variable `GANDI_KEY` will be needed to run ExternalDNS with Gandi.
|
||||
|
||||
## Deploy ExternalDNS
|
||||
|
||||
Connect your `kubectl` client to the cluster you want to test ExternalDNS with.
|
||||
Then apply one of the following manifests file to deploy ExternalDNS.
|
||||
|
||||
### Manifest (for clusters without RBAC enabled)
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: external-dns
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: external-dns
|
||||
strategy:
|
||||
type: Recreate
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: external-dns
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.7
|
||||
args:
|
||||
- --source=service # ingress is also possible
|
||||
- --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
|
||||
- --provider=gandi
|
||||
env:
|
||||
- name: GANDI_KEY
|
||||
value: "YOUR_GANDI_API_KEY"
|
||||
```
|
||||
|
||||
### Manifest (for clusters with RBAC enabled)
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: external-dns
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: external-dns
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["services","endpoints","pods"]
|
||||
verbs: ["get","watch","list"]
|
||||
- apiGroups: ["extensions","networking.k8s.io"]
|
||||
resources: ["ingresses"]
|
||||
verbs: ["get","watch","list"]
|
||||
- apiGroups: [""]
|
||||
resources: ["nodes"]
|
||||
verbs: ["list","watch"]
|
||||
---
|
||||
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: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: external-dns
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: external-dns
|
||||
strategy:
|
||||
type: Recreate
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: external-dns
|
||||
spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.7
|
||||
args:
|
||||
- --source=service # ingress is also possible
|
||||
- --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
|
||||
- --provider=gandi
|
||||
env:
|
||||
- name: GANDI_KEY
|
||||
value: "YOUR_GANDI_API_KEY"
|
||||
```
|
||||
|
||||
|
||||
## Deploying an Nginx Service
|
||||
|
||||
Create a service file called 'nginx.yaml' with the following contents:
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nginx
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- image: nginx
|
||||
name: nginx
|
||||
ports:
|
||||
- containerPort: 80
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: nginx
|
||||
annotations:
|
||||
external-dns.alpha.kubernetes.io/hostname: my-app.example.com
|
||||
spec:
|
||||
selector:
|
||||
app: nginx
|
||||
type: LoadBalancer
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 80
|
||||
```
|
||||
|
||||
Note the annotation on the service; use the same hostname as the Gandi Domain. Make sure that your Domain is configured to use Live-DNS.
|
||||
|
||||
ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records.
|
||||
|
||||
Create the deployment and service:
|
||||
|
||||
```console
|
||||
$ kubectl create -f nginx.yaml
|
||||
```
|
||||
|
||||
Depending where you run your service it can take a little while for your cloud provider to create an external IP for the service.
|
||||
|
||||
Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize the Gandi DNS records.
|
||||
|
||||
## Verifying Gandi DNS records
|
||||
|
||||
Check your [Gandi Dashboard](https://admin.gandi.net/domain) to view the records for your Gandi DNS zone.
|
||||
|
||||
Click on the zone for the one created above if a different domain was used.
|
||||
|
||||
This should show the external IP address of the service as the A record for your domain.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Now that we have verified that ExternalDNS will automatically manage Gandi DNS records, we can delete the tutorial's example:
|
||||
|
||||
```
|
||||
$ kubectl delete service -f nginx.yaml
|
||||
$ kubectl delete service -f externaldns.yaml
|
||||
```
|
||||
|
||||
# Additional options
|
||||
|
||||
If you're using organizations to separate your domains, you can pass the organization's ID in an environment variable called `GANDI_SHARING_ID` to get access to it.
|
@ -70,7 +70,7 @@ kind: ServiceAccount
|
||||
metadata:
|
||||
name: external-dns
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: external-dns
|
||||
@ -85,7 +85,7 @@ rules:
|
||||
resources: ["nodes"]
|
||||
verbs: ["get", "watch", "list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: external-dns-viewer
|
||||
@ -116,7 +116,7 @@ spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.8.0
|
||||
args:
|
||||
- --source=service
|
||||
- --source=ingress
|
||||
@ -211,7 +211,7 @@ $ curl nginx.external-dns-test.gcp.zalan.do
|
||||
Let's check that Ingress works as well. Create the following Ingress.
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: nginx
|
||||
@ -439,7 +439,7 @@ spec:
|
||||
- --google-project=zalando-external-dns-test
|
||||
- --registry=txt
|
||||
- --txt-owner-id=my-identifier
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.8.0
|
||||
name: external-dns
|
||||
securityContext:
|
||||
fsGroup: 65534
|
||||
@ -460,7 +460,7 @@ $ kubectl annotate serviceaccount --namespace=external-dns external-dns \
|
||||
Create the following sample application to test that ExternalDNS works.
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: nginx
|
||||
|
101
docs/tutorials/gloo-proxy.md
Normal file
101
docs/tutorials/gloo-proxy.md
Normal file
@ -0,0 +1,101 @@
|
||||
# Configuring ExternalDNS to use the Gloo Proxy Source
|
||||
This tutorial describes how to configure ExternalDNS to use the Gloo Proxy source.
|
||||
It is meant to supplement the other provider-specific setup tutorials.
|
||||
|
||||
### Manifest (for clusters without RBAC enabled)
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: external-dns
|
||||
spec:
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: external-dns
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: external-dns
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
# update this to the desired external-dns version
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
args:
|
||||
- --source=gloo-proxy
|
||||
- --gloo-namespace=custom-gloo-system # gloo system namespace. Omit to use the default (gloo-system)
|
||||
- --provider=aws
|
||||
- --registry=txt
|
||||
- --txt-owner-id=my-identifier
|
||||
```
|
||||
|
||||
### Manifest (for clusters with RBAC enabled)
|
||||
Could be change if you have mulitple sources
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: external-dns
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: external-dns
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["services","endpoints","pods"]
|
||||
verbs: ["get","watch","list"]
|
||||
- apiGroups: [""]
|
||||
resources: ["nodes"]
|
||||
verbs: ["list","watch"]
|
||||
- apiGroups: ["gloo.solo.io"]
|
||||
resources: ["proxies"]
|
||||
verbs: ["get","watch","list"]
|
||||
- apiGroups: ["gateway.solo.io"]
|
||||
resources: ["virtualservices"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
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: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: external-dns
|
||||
spec:
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: external-dns
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: external-dns
|
||||
spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
# update this to the desired external-dns version
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
args:
|
||||
- --source=gloo-proxy
|
||||
- --gloo-namespace=custom-gloo-system # gloo system namespace. Omit to use the default (gloo-system)
|
||||
- --provider=aws
|
||||
- --registry=txt
|
||||
- --txt-owner-id=my-identifier
|
||||
```
|
||||
|
197
docs/tutorials/godaddy.md
Normal file
197
docs/tutorials/godaddy.md
Normal file
@ -0,0 +1,197 @@
|
||||
# Setting up ExternalDNS for Services on GoDaddy
|
||||
|
||||
This tutorial describes how to setup ExternalDNS for use within a
|
||||
Kubernetes cluster using GoDaddy DNS.
|
||||
|
||||
Make sure to use **>=0.6** version of ExternalDNS for this tutorial.
|
||||
|
||||
## Creating a zone with GoDaddy DNS
|
||||
|
||||
If you are new to GoDaddy, we recommend you first read the following
|
||||
instructions for creating a zone.
|
||||
|
||||
[Creating a zone using the GoDaddy web console](https://www.godaddy.com/)
|
||||
|
||||
[Creating a zone using the GoDaddy API](https://developer.godaddy.com/)
|
||||
|
||||
## Creating GoDaddy API key
|
||||
|
||||
You first need to create an API Key.
|
||||
|
||||
Using the [GoDaddy documentation](https://developer.godaddy.com/getstarted) you will have your `API key` and `API secret`
|
||||
|
||||
## Deploy ExternalDNS
|
||||
|
||||
Connect your `kubectl` client to the cluster with which you want to test ExternalDNS, and then apply one of the following manifest files for deployment:
|
||||
|
||||
### Manifest (for clusters without RBAC enabled)
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: external-dns
|
||||
spec:
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: external-dns
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: external-dns
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.7
|
||||
args:
|
||||
- --source=service # ingress is also possible
|
||||
- --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
|
||||
- --provider=godaddy
|
||||
- --txt-prefix=external-dns. # In case of multiple k8s cluster
|
||||
- --txt-owner-id=owner-id # In case of multiple k8s cluster
|
||||
- --godaddy-api-key=<Your API Key>
|
||||
- --godaddy-api-secret=<Your API secret>
|
||||
```
|
||||
|
||||
### Manifest (for clusters with RBAC enabled)
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: external-dns
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: external-dns
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["services"]
|
||||
verbs: ["get","watch","list"]
|
||||
- apiGroups: [""]
|
||||
resources: ["pods"]
|
||||
verbs: ["get","watch","list"]
|
||||
- apiGroups: ["extensions","networking.k8s.io"]
|
||||
resources: ["ingresses"]
|
||||
verbs: ["get","watch","list"]
|
||||
- apiGroups: [""]
|
||||
resources: ["nodes"]
|
||||
verbs: ["list","watch"]
|
||||
- apiGroups: [""]
|
||||
resources: ["endpoints"]
|
||||
verbs: ["get","watch","list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
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: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: external-dns
|
||||
spec:
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: external-dns
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: external-dns
|
||||
spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.7
|
||||
args:
|
||||
- --source=service # ingress is also possible
|
||||
- --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
|
||||
- --provider=godaddy
|
||||
- --txt-prefix=external-dns. # In case of multiple k8s cluster
|
||||
- --txt-owner-id=owner-id # In case of multiple k8s cluster
|
||||
- --godaddy-api-key=<Your API Key>
|
||||
- --godaddy-api-secret=<Your API secret>
|
||||
```
|
||||
|
||||
## Deploying an Nginx Service
|
||||
|
||||
Create a service file called 'nginx.yaml' with the following contents:
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nginx
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- image: nginx
|
||||
name: nginx
|
||||
ports:
|
||||
- containerPort: 80
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: nginx
|
||||
annotations:
|
||||
external-dns.alpha.kubernetes.io/hostname: example.com
|
||||
external-dns.alpha.kubernetes.io/ttl: "120" #optional
|
||||
spec:
|
||||
selector:
|
||||
app: nginx
|
||||
type: LoadBalancer
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 80
|
||||
```
|
||||
|
||||
**A note about annotations**
|
||||
|
||||
Verify that the annotation on the service uses the same hostname as the GoDaddy DNS zone created above. The annotation may also be a subdomain of the DNS zone (e.g. 'www.example.com').
|
||||
|
||||
The TTL annotation can be used to configure the TTL on DNS records managed by ExternalDNS and is optional. If this annotation is not set, the TTL on records managed by ExternalDNS will default to 10.
|
||||
|
||||
ExternalDNS uses the hostname annotation to determine which services should be registered with DNS. Removing the hostname annotation will cause ExternalDNS to remove the corresponding DNS records.
|
||||
|
||||
### Create the deployment and service
|
||||
|
||||
```
|
||||
$ kubectl create -f nginx.yaml
|
||||
```
|
||||
|
||||
Depending on where you run your service, it may take some time for your cloud provider to create an external IP for the service. Once an external IP is assigned, ExternalDNS detects the new service IP address and synchronizes the GoDaddy DNS records.
|
||||
|
||||
## Verifying GoDaddy DNS records
|
||||
|
||||
Use the GoDaddy web console or API to verify that the A record for your domain shows the external IP address of the services.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Once you successfully configure and verify record management via ExternalDNS, you can delete the tutorial's example:
|
||||
|
||||
```
|
||||
$ kubectl delete -f nginx.yaml
|
||||
$ kubectl delete -f externaldns.yaml
|
||||
```
|
@ -2,7 +2,7 @@
|
||||
|
||||
This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using Hetzner DNS.
|
||||
|
||||
Make sure to use **>=0.7.3** version of ExternalDNS for this tutorial.
|
||||
Make sure to use **>=0.7.6** version of ExternalDNS for this tutorial.
|
||||
|
||||
## Creating a Hetzner DNS zone
|
||||
|
||||
@ -43,7 +43,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
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.
|
||||
@ -60,7 +60,7 @@ kind: ServiceAccount
|
||||
metadata:
|
||||
name: external-dns
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: external-dns
|
||||
@ -75,7 +75,7 @@ rules:
|
||||
resources: ["nodes"]
|
||||
verbs: ["list","watch"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: external-dns-viewer
|
||||
@ -107,7 +107,7 @@ spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
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.
|
||||
|
@ -31,7 +31,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
args:
|
||||
- --log-level=debug
|
||||
- --source=service
|
||||
@ -50,7 +50,7 @@ kind: ServiceAccount
|
||||
metadata:
|
||||
name: external-dns
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: external-dns
|
||||
@ -65,7 +65,7 @@ rules:
|
||||
resources: ["nodes"]
|
||||
verbs: ["list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: external-dns-viewer
|
||||
@ -96,7 +96,7 @@ spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
args:
|
||||
- --log-level=debug
|
||||
- --source=service
|
||||
|
@ -69,7 +69,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
args:
|
||||
- --source=service
|
||||
- --domain-filter=example.com # (optional) limit to only example.com domains.
|
||||
@ -103,7 +103,7 @@ kind: ServiceAccount
|
||||
metadata:
|
||||
name: external-dns
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: external-dns
|
||||
@ -118,7 +118,7 @@ rules:
|
||||
resources: ["nodes"]
|
||||
verbs: ["list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: external-dns-viewer
|
||||
@ -149,7 +149,7 @@ spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
args:
|
||||
- --source=service
|
||||
- --domain-filter=example.com # (optional) limit to only example.com domains.
|
||||
@ -260,3 +260,11 @@ $ curl -kl \
|
||||
-u ${WAPI_USERNAME}:${WAPI_PASSWORD} \
|
||||
https://${GRID_HOST}:${WAPI_PORT}/wapi/v${WAPI_VERSION}/zone_auth?fqdn=example.com
|
||||
```
|
||||
|
||||
## Ability to filter results from the zone auth API using a regular expression
|
||||
|
||||
There is also the ability to filter results from the Infoblox zone_auth service based upon a regular expression. See the [Infoblox API document](https://www.infoblox.com/wp-content/uploads/infoblox-deployment-infoblox-rest-api.pdf) for examples. To use this feature for the zone_auth service, set the parameter infoblox-fqdn-regex for external-dns to a regular expression that makes sense for you. For instance, to only return hosted zones that start with staging in the test.com domain (like staging.beta.test.com, or staging.test.com), use the following command line option when starting external-dns
|
||||
|
||||
```
|
||||
--infoblox-fqdn-regex=^staging.*test.com$
|
||||
```
|
||||
|
@ -28,7 +28,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
args:
|
||||
- --source=service
|
||||
- --source=ingress
|
||||
@ -98,7 +98,7 @@ spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
args:
|
||||
- --source=service
|
||||
- --source=ingress
|
||||
|
95
docs/tutorials/kong.md
Normal file
95
docs/tutorials/kong.md
Normal file
@ -0,0 +1,95 @@
|
||||
# Configuring ExternalDNS to use the Kong TCPIngress Source
|
||||
This tutorial describes how to configure ExternalDNS to use the Kong TCPIngress source.
|
||||
It is meant to supplement the other provider-specific setup tutorials.
|
||||
|
||||
### Manifest (for clusters without RBAC enabled)
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: external-dns
|
||||
spec:
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: external-dns
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: external-dns
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
# update this to the desired external-dns version
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.9.0
|
||||
args:
|
||||
- --source=kong-tcpingress
|
||||
- --provider=aws
|
||||
- --registry=txt
|
||||
- --txt-owner-id=my-identifier
|
||||
```
|
||||
|
||||
### Manifest (for clusters with RBAC enabled)
|
||||
Could be changed if you have mulitple sources
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: external-dns
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: external-dns
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["services","endpoints","pods"]
|
||||
verbs: ["get","watch","list"]
|
||||
- apiGroups: [""]
|
||||
resources: ["nodes"]
|
||||
verbs: ["list","watch"]
|
||||
- apiGroups: ["configuration.konghq.com"]
|
||||
resources: ["tcpingresses"]
|
||||
verbs: ["get","watch","list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
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: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: external-dns
|
||||
spec:
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: external-dns
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: external-dns
|
||||
spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
# update this to the desired external-dns version
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.9.0
|
||||
args:
|
||||
- --source=kong-tcpingress
|
||||
- --provider=aws
|
||||
- --registry=txt
|
||||
- --txt-owner-id=my-identifier
|
||||
```
|
37
docs/tutorials/kops-dns-controller.md
Normal file
37
docs/tutorials/kops-dns-controller.md
Normal file
@ -0,0 +1,37 @@
|
||||
# kOps dns-controller compatibility mode
|
||||
|
||||
kOps includes a dns-controller that is primarily used to bootstrap the cluster, but can also be used for provisioning DNS entries for Services and Ingress.
|
||||
|
||||
ExternalDNS can be used as a drop-in replacement for dns-controller if you are running a non-gossip cluster. The flag `--compatibility kops-dns-controller` enables the dns-controller behaviour.
|
||||
|
||||
## Annotations
|
||||
|
||||
In kops-dns-controller compatibility mode, ExternalDNS supports two additional annotations:
|
||||
|
||||
* `dns.alpha.kubernetes.io/external` which is used to define a DNS record for accessing the resource publicly (i.e. public IPs)
|
||||
|
||||
* `dns.alpha.kubernetes.io/internal` which is used to define a DNS record for accessing the resource from outside the cluster but inside the cloud,
|
||||
i.e. it will typically use internal IPs for instances.
|
||||
|
||||
These annotations may both be comma-separated lists of names.
|
||||
|
||||
## DNS record mappings
|
||||
|
||||
The DNS record mappings try to "do the right thing", but what this means is different for each resource type.
|
||||
|
||||
### Pods
|
||||
|
||||
For the external annotation, ExternalDNS will map a HostNetwork=true Pod to the external IPs of the Node.
|
||||
|
||||
For the internal annotation, ExternalDNS will map a HostNetwork=true Pod to the internal IPs of the Node.
|
||||
|
||||
ExternalDNS ignore Pods that are not HostNetwork=true
|
||||
|
||||
Annotations added to Pods will always result in an A record being created.
|
||||
|
||||
### Services
|
||||
|
||||
* For a Service of Type=LoadBalancer, ExternalDNS looks at Status.LoadBalancer.Ingress. It will create CNAMEs to hostnames,
|
||||
and A records for IP addresses. It will do this for both internal and external names
|
||||
|
||||
* For a Service of Type=NodePort, ExternalDNS will create A records for the Node's internal/external IP addresses, as appropriate.
|
@ -36,7 +36,7 @@ This depends on your RBAC policies, in case you use RBAC, you can use
|
||||
this for all 3 controllers:
|
||||
|
||||
```yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: kube-ingress-aws-controller
|
||||
@ -138,7 +138,7 @@ default.
|
||||
Create the following Ingress to expose the echoserver application to the Internet.
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
@ -172,7 +172,7 @@ this Ingress object will only be fronting one backend Service, we might instead
|
||||
create the following:
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
@ -205,7 +205,7 @@ and one AAAA record) for each hostname associated with the Ingress object.
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
@ -239,7 +239,7 @@ set to `nlb` then ExternalDNS will create an NLB instead of an ALB.
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
|
@ -41,7 +41,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
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.
|
||||
@ -59,7 +59,7 @@ kind: ServiceAccount
|
||||
metadata:
|
||||
name: external-dns
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: external-dns
|
||||
@ -74,7 +74,7 @@ rules:
|
||||
resources: ["nodes"]
|
||||
verbs: ["list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: external-dns-viewer
|
||||
@ -105,7 +105,7 @@ spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
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.
|
||||
|
@ -227,7 +227,7 @@ kind: ServiceAccount
|
||||
metadata:
|
||||
name: external-dns
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: external-dns
|
||||
@ -242,7 +242,7 @@ rules:
|
||||
resources: ["nodes"]
|
||||
verbs: ["list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: external-dns-viewer
|
||||
@ -273,7 +273,7 @@ spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
args:
|
||||
- --source=ingress
|
||||
- --domain-filter=external-dns-test.gcp.zalan.do
|
||||
@ -290,7 +290,7 @@ Use `--dry-run` if you want to be extra careful on the first run. Note, that you
|
||||
Create the following sample application to test that ExternalDNS works.
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: nginx
|
||||
@ -565,7 +565,7 @@ spec:
|
||||
- --google-project=zalando-external-dns-test
|
||||
- --registry=txt
|
||||
- --txt-owner-id=my-identifier
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
name: external-dns
|
||||
securityContext:
|
||||
fsGroup: 65534
|
||||
@ -586,7 +586,7 @@ $ kubectl annotate serviceaccount --namespace=external-dns external-dns \
|
||||
Create the following sample application to test that ExternalDNS works.
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: nginx
|
||||
|
@ -61,7 +61,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
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.
|
||||
@ -79,7 +79,7 @@ kind: ServiceAccount
|
||||
metadata:
|
||||
name: external-dns
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: external-dns
|
||||
@ -94,7 +94,7 @@ rules:
|
||||
resources: ["nodes"]
|
||||
verbs: ["list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: external-dns-viewer
|
||||
@ -125,7 +125,7 @@ spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
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.
|
||||
|
@ -25,7 +25,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
args:
|
||||
- --source=openshift-route
|
||||
- --domain-filter=external-dns-test.my-org.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones
|
||||
@ -43,7 +43,7 @@ kind: ServiceAccount
|
||||
metadata:
|
||||
name: external-dns
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: external-dns
|
||||
@ -61,7 +61,7 @@ rules:
|
||||
resources: ["routes"]
|
||||
verbs: ["get","watch","list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: external-dns-viewer
|
||||
@ -92,7 +92,7 @@ spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
args:
|
||||
- --source=openshift-route
|
||||
- --domain-filter=external-dns-test.my-org.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones
|
||||
|
@ -78,7 +78,7 @@ kind: ServiceAccount
|
||||
metadata:
|
||||
name: external-dns
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: external-dns
|
||||
@ -93,7 +93,7 @@ rules:
|
||||
resources: ["nodes"]
|
||||
verbs: ["list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: external-dns-viewer
|
||||
@ -124,7 +124,7 @@ spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
args:
|
||||
- --source=service
|
||||
- --source=ingress
|
||||
|
@ -86,7 +86,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
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.
|
||||
@ -108,7 +108,7 @@ kind: ServiceAccount
|
||||
metadata:
|
||||
name: external-dns
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: external-dns
|
||||
@ -129,7 +129,7 @@ rules:
|
||||
resources: ["endpoints"]
|
||||
verbs: ["get","watch","list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: external-dns-viewer
|
||||
@ -160,7 +160,7 @@ spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
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.
|
||||
|
@ -42,7 +42,7 @@ spec:
|
||||
# serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
args:
|
||||
- --source=service # or ingress or both
|
||||
- --provider=pdns
|
||||
@ -54,13 +54,15 @@ spec:
|
||||
- --interval=30s
|
||||
```
|
||||
|
||||
#### Domain Filter (--domain-filter)
|
||||
When the domain-filter argument is specified, external-dns will automatically create DNS records based on host names specified in ingress objects and services with the external-dns annotation that match the domain-filter argument in the external-dns deployment manifest.
|
||||
#### Domain Filter (`--domain-filter`)
|
||||
When the `--domain-filter` argument is specified, external-dns will only create DNS records for host names (specified in ingress objects and services with the external-dns annotation) related to zones that match the `--domain-filter` argument in the external-dns deployment manifest.
|
||||
|
||||
eg. ```--domain-filter=example.org``` will allow for zone `example.org` and any zones in PowerDNS that ends in `.example.org`, including `an.example.org`, ie. the subdomains of example.org.
|
||||
|
||||
eg. ```--domain-filter=.example.org``` will allow *only* zones that end in `.example.org`, ie. the subdomains of example.org but not the `example.org` zone itself.
|
||||
|
||||
The filter can also match parent zones. For example `--domain-filter=a.example.com` will allow for zone `example.com`. If you want to match parent zones, you cannot pre-pend your filter with a ".", eg. `--domain-filter=.example.com` will not attempt to match parent zones.
|
||||
|
||||
## RBAC
|
||||
|
||||
If your cluster is RBAC enabled, you also need to setup the following, before you can run external-dns:
|
||||
@ -70,7 +72,7 @@ kind: ServiceAccount
|
||||
metadata:
|
||||
name: external-dns
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: external-dns
|
||||
@ -88,7 +90,7 @@ rules:
|
||||
resources: ["nodes"]
|
||||
verbs: ["list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: external-dns-viewer
|
||||
|
@ -241,9 +241,9 @@ spec:
|
||||
- --provider=aws
|
||||
- --registry=txt
|
||||
- --txt-owner-id=external-dns
|
||||
- --annotation-filter=kubernetes.io/ingress.class=external-ingress
|
||||
- --annotation-filter=kubernetes.io/ingress.class in (external-ingress)
|
||||
- --aws-zone-type=public
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
name: external-dns-public
|
||||
```
|
||||
|
||||
@ -279,9 +279,9 @@ spec:
|
||||
- --provider=aws
|
||||
- --registry=txt
|
||||
- --txt-owner-id=dev.k8s.nexus
|
||||
- --annotation-filter=kubernetes.io/ingress.class=internal-ingress
|
||||
- --annotation-filter=kubernetes.io/ingress.class in (internal-ingress)
|
||||
- --aws-zone-type=private
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
name: external-dns-private
|
||||
```
|
||||
|
||||
@ -292,7 +292,7 @@ For this setup to work, you've to create two Service definitions for your applic
|
||||
At first, create public Service definition:
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
@ -313,7 +313,7 @@ spec:
|
||||
Then create private Service definition:
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
@ -334,7 +334,7 @@ spec:
|
||||
Additionally, you may leverage [cert-manager](https://github.com/jetstack/cert-manager) to automatically issue SSL certificates from [Let's Encrypt](https://letsencrypt.org/). To do that, request a certificate in public service definition:
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
@ -363,7 +363,7 @@ spec:
|
||||
And reuse the requested certificate in private Service definition:
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
|
@ -53,7 +53,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
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.
|
||||
@ -74,7 +74,7 @@ kind: ServiceAccount
|
||||
metadata:
|
||||
name: external-dns
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: external-dns
|
||||
@ -89,7 +89,7 @@ rules:
|
||||
resources: ["nodes"]
|
||||
verbs: ["list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: external-dns-viewer
|
||||
@ -120,7 +120,7 @@ spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
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.
|
||||
|
@ -54,7 +54,7 @@ spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
args:
|
||||
- --source=ingress
|
||||
- --provider=rdns
|
||||
@ -70,7 +70,7 @@ spec:
|
||||
```yaml
|
||||
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: external-dns
|
||||
@ -85,7 +85,7 @@ rules:
|
||||
resources: ["nodes"]
|
||||
verbs: ["list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: external-dns-viewer
|
||||
@ -123,7 +123,7 @@ spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
args:
|
||||
- --source=ingress
|
||||
- --provider=rdns
|
||||
@ -138,7 +138,7 @@ spec:
|
||||
## Testing ingress example
|
||||
```
|
||||
$ cat ingress.yaml
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: nginx
|
||||
|
@ -94,7 +94,7 @@ spec:
|
||||
selector:
|
||||
app: nginx
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: my-ingress
|
||||
@ -154,7 +154,7 @@ metadata:
|
||||
labels:
|
||||
name: external-dns
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: external-dns
|
||||
@ -187,7 +187,7 @@ metadata:
|
||||
name: external-dns
|
||||
namespace: external-dns
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: external-dns-viewer
|
||||
@ -218,8 +218,10 @@ spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
args:
|
||||
- --registry=txt
|
||||
- --txt-prefix=external-dns-
|
||||
- --txt-owner-id=k8s
|
||||
- --provider=rfc2136
|
||||
- --rfc2136-host=192.168.0.1
|
||||
@ -258,8 +260,10 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
args:
|
||||
- --registry=txt
|
||||
- --txt-prefix=external-dns-
|
||||
- --txt-owner-id=k8s
|
||||
- --provider=rfc2136
|
||||
- --rfc2136-host=192.168.0.1
|
||||
@ -273,17 +277,19 @@ spec:
|
||||
- --domain-filter=k8s.example.org
|
||||
```
|
||||
|
||||
## Microsoft DNS
|
||||
## Microsoft DNS (Insecure Updates)
|
||||
|
||||
While `external-dns` was not developed or tested against Microsoft DNS, it can be configured to work against it. YMMV.
|
||||
|
||||
### DNS-side configuration
|
||||
### Insecure Updates
|
||||
|
||||
#### DNS-side configuration
|
||||
|
||||
1. Create a DNS zone
|
||||
2. Enable insecure dynamic updates for the zone
|
||||
3. Enable Zone Transfers from all servers
|
||||
3. Enable Zone Transfers to all servers
|
||||
|
||||
### `external-dns` configuration
|
||||
#### `external-dns` configuration
|
||||
|
||||
You'll want to configure `external-dns` similarly to the following:
|
||||
|
||||
@ -298,4 +304,98 @@ You'll want to configure `external-dns` similarly to the following:
|
||||
...
|
||||
```
|
||||
|
||||
Since Microsoft DNS does not support secure updates via TSIG, this will let `external-dns` make insecure updates. Do this at your own risk.
|
||||
### Secure Updates Using RFC3645 (GSS-TSIG)
|
||||
|
||||
### DNS-side configuration
|
||||
|
||||
1. Create a DNS zone
|
||||
2. Enable secure dynamic updates for the zone
|
||||
3. Enable Zone Transfers to all servers
|
||||
|
||||
If you see any error messages which indicate that `external-dns` was somehow not able to fetch
|
||||
existing DNS records from your DNS server, this could mean that you forgot about step 3.
|
||||
|
||||
#### Kerberos Configuration
|
||||
|
||||
DNS with secure updates relies upon a valid Kerberos configuration running within the `external-dns` container. At this time, you will need to create a ConfigMap for the `external-dns` container to use and mount it in your deployment. Below is an example of a working Kerberos configuration inside a ConfigMap definition. This may be different depending on many factors in your environment:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
name: krb5.conf
|
||||
data:
|
||||
krb5.conf: |
|
||||
[logging]
|
||||
default = FILE:/var/log/krb5libs.log
|
||||
kdc = FILE:/var/log/krb5kdc.log
|
||||
admin_server = FILE:/var/log/kadmind.log
|
||||
|
||||
[libdefaults]
|
||||
dns_lookup_realm = false
|
||||
ticket_lifetime = 24h
|
||||
renew_lifetime = 7d
|
||||
forwardable = true
|
||||
rdns = false
|
||||
pkinit_anchors = /etc/pki/tls/certs/ca-bundle.crt
|
||||
default_ccache_name = KEYRING:persistent:%{uid}
|
||||
|
||||
default_realm = YOUR-REALM.COM
|
||||
|
||||
[realms]
|
||||
YOUR-REALM.COM = {
|
||||
kdc = dc1.yourdomain.com
|
||||
admin_server = dc1.yourdomain.com
|
||||
}
|
||||
|
||||
[domain_realm]
|
||||
yourdomain.com = YOUR-REALM.COM
|
||||
.yourdomain.com = YOUR-REALM.COM
|
||||
```
|
||||
In most cases, the realm name will probably be the same as the domain name, so you can simply replace
|
||||
`YOUR-REALM.COM` with something like `YOURDOMAIN.COM`.
|
||||
|
||||
Once the ConfigMap is created, the container `external-dns` container needs to be told to mount that ConfigMap as a volume at the default Kerberos configuration location. The pod spec should include a similar configuration to the following:
|
||||
|
||||
```yaml
|
||||
...
|
||||
volumeMounts:
|
||||
- mountPath: /etc/krb5.conf
|
||||
name: kerberos-config-volume
|
||||
subPath: krb5.conf
|
||||
...
|
||||
volumes:
|
||||
- configMap:
|
||||
defaultMode: 420
|
||||
name: krb5.conf
|
||||
name: kerberos-config-volume
|
||||
...
|
||||
```
|
||||
|
||||
#### `external-dns` configuration
|
||||
|
||||
You'll want to configure `external-dns` similarly to the following:
|
||||
|
||||
```text
|
||||
...
|
||||
- --provider=rfc2136
|
||||
- --rfc2136-gss-tsig
|
||||
- --rfc2136-host=dns-host.yourdomain.com
|
||||
- --rfc2136-port=53
|
||||
- --rfc2136-zone=your-zone.com
|
||||
- --rfc2136-kerberos-username=your-domain-account
|
||||
- --rfc2136-kerberos-password=your-domain-password
|
||||
- --rfc2136-kerberos-realm=your-domain.com
|
||||
- --rfc2136-tsig-axfr # needed to enable zone transfers, which is required for deletion of records.
|
||||
...
|
||||
```
|
||||
|
||||
As noted above, the `--rfc2136-kerberos-realm` flag is completely optional and won't be necessary in many cases.
|
||||
Most likely, you will only need it if you see errors similar to this: `KRB Error: (68) KDC_ERR_WRONG_REALM Reserved for future use`.
|
||||
|
||||
The flag `--rfc2136-host` can be set to the host's domain name or IP address.
|
||||
However, it also determines the name of the Kerberos principal which is used during authentication.
|
||||
This means that Active Directory might only work if this is set to a specific domain name, possibly leading to errors like this:
|
||||
`KDC_ERR_S_PRINCIPAL_UNKNOWN Server not found in Kerberos database`.
|
||||
To fix this, try setting `--rfc2136-host` to the "actual" hostname of your DNS server.
|
||||
|
@ -24,7 +24,6 @@ Note that you will also need to the Organization ID, which can be retrieve on th
|
||||
Three environment variables are needed to run ExternalDNS with Scaleway DNS:
|
||||
- `SCW_ACCESS_KEY` which is the Access Key.
|
||||
- `SCW_SECRET_KEY` which is the Secret Key.
|
||||
- `SCW_DEFAULT_ORGANIZATION_ID` which is your Organization ID.
|
||||
|
||||
## Deploy ExternalDNS
|
||||
|
||||
@ -53,7 +52,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.4
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.8.0
|
||||
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.
|
||||
@ -63,8 +62,6 @@ spec:
|
||||
value: "<your access key>"
|
||||
- name: SCW_SECRET_KEY
|
||||
value: "<your secret key>"
|
||||
- name: SCW_DEFAULT_ORGANIZATION_ID
|
||||
value: "<your organization ID>"
|
||||
```
|
||||
|
||||
### Manifest (for clusters with RBAC enabled)
|
||||
@ -74,7 +71,7 @@ kind: ServiceAccount
|
||||
metadata:
|
||||
name: external-dns
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: external-dns
|
||||
@ -89,7 +86,7 @@ rules:
|
||||
resources: ["nodes"]
|
||||
verbs: ["list","watch"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: external-dns-viewer
|
||||
@ -121,7 +118,7 @@ spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.4
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.8.0
|
||||
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.
|
||||
@ -131,8 +128,6 @@ spec:
|
||||
value: "<your access key>"
|
||||
- name: SCW_SECRET_KEY
|
||||
value: "<your secret key>"
|
||||
- name: SCW_DEFAULT_ORGANIZATION_ID
|
||||
value: "<your organization ID>"
|
||||
```
|
||||
|
||||
|
||||
|
@ -20,7 +20,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
args:
|
||||
- ... # your arguments here
|
||||
securityContext:
|
||||
|
@ -36,7 +36,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
args:
|
||||
- --source=service # ingress is also possible
|
||||
- --domain-filter=example.com # (optional) limit to only example.com domains
|
||||
@ -61,7 +61,7 @@ kind: ServiceAccount
|
||||
metadata:
|
||||
name: external-dns
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: external-dns
|
||||
@ -76,7 +76,7 @@ rules:
|
||||
resources: ["nodes"]
|
||||
verbs: ["watch", "list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: external-dns-viewer
|
||||
@ -107,7 +107,7 @@ spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
args:
|
||||
- --source=service # ingress is also possible
|
||||
- --domain-filter=example.com # (optional) limit to only example.com domains
|
||||
|
@ -44,7 +44,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
args:
|
||||
- --source=service
|
||||
- --source=ingress # ingress is also possible
|
||||
@ -70,7 +70,7 @@ kind: ServiceAccount
|
||||
metadata:
|
||||
name: external-dns
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: external-dns
|
||||
@ -85,7 +85,7 @@ rules:
|
||||
resources: ["nodes"]
|
||||
verbs: ["list","watch"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: external-dns-viewer
|
||||
@ -116,7 +116,7 @@ spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
args:
|
||||
- --source=service
|
||||
- --source=ingress
|
||||
|
@ -66,7 +66,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
args:
|
||||
- --provider=vinyldns
|
||||
- --source=service
|
||||
@ -91,7 +91,7 @@ kind: ServiceAccount
|
||||
metadata:
|
||||
name: external-dns
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: external-dns
|
||||
@ -106,7 +106,7 @@ rules:
|
||||
resources: ["nodes"]
|
||||
verbs: ["list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: external-dns-viewer
|
||||
@ -137,7 +137,7 @@ spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
args:
|
||||
- --provider=vinyldns
|
||||
- --source=service
|
||||
|
@ -42,7 +42,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
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.
|
||||
@ -60,7 +60,7 @@ kind: ServiceAccount
|
||||
metadata:
|
||||
name: external-dns
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: external-dns
|
||||
@ -75,7 +75,7 @@ rules:
|
||||
resources: ["nodes"]
|
||||
verbs: ["list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: external-dns-viewer
|
||||
@ -106,7 +106,7 @@ spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
|
||||
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.
|
||||
|
@ -17,15 +17,58 @@ limitations under the License.
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// DomainFilterInterface defines the interface to select matching domains for a specific provider or runtime
|
||||
type DomainFilterInterface interface {
|
||||
Match(domain string) bool
|
||||
IsConfigured() bool
|
||||
}
|
||||
|
||||
type MatchAllDomainFilters []DomainFilterInterface
|
||||
|
||||
func (f MatchAllDomainFilters) Match(domain string) bool {
|
||||
if !f.IsConfigured() {
|
||||
return true
|
||||
}
|
||||
for _, filter := range f {
|
||||
if filter == nil {
|
||||
continue
|
||||
}
|
||||
if filter.IsConfigured() && !filter.Match(domain) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (f MatchAllDomainFilters) IsConfigured() bool {
|
||||
if f == nil {
|
||||
return false
|
||||
}
|
||||
for _, filter := range f {
|
||||
if filter == nil {
|
||||
continue
|
||||
}
|
||||
if filter.IsConfigured() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return len(f) > 0
|
||||
}
|
||||
|
||||
// DomainFilter holds a lists of valid domain names
|
||||
type DomainFilter struct {
|
||||
// Filters define what domains to match
|
||||
Filters []string
|
||||
// exclude define what domains not to match
|
||||
exclude []string
|
||||
// regex defines a regular expression to match the domains
|
||||
regex *regexp.Regexp
|
||||
// regexExclusion defines a regular expression to exclude the domains matched
|
||||
regexExclusion *regexp.Regexp
|
||||
}
|
||||
|
||||
// prepareFilters provides consistent trimming for filters/exclude params
|
||||
@ -39,16 +82,26 @@ func prepareFilters(filters []string) []string {
|
||||
|
||||
// NewDomainFilterWithExclusions returns a new DomainFilter, given a list of matches and exclusions
|
||||
func NewDomainFilterWithExclusions(domainFilters []string, excludeDomains []string) DomainFilter {
|
||||
return DomainFilter{prepareFilters(domainFilters), prepareFilters(excludeDomains)}
|
||||
return DomainFilter{Filters: prepareFilters(domainFilters), exclude: prepareFilters(excludeDomains)}
|
||||
}
|
||||
|
||||
// NewDomainFilter returns a new DomainFilter given a comma separated list of domains
|
||||
func NewDomainFilter(domainFilters []string) DomainFilter {
|
||||
return DomainFilter{prepareFilters(domainFilters), []string{}}
|
||||
return DomainFilter{Filters: prepareFilters(domainFilters)}
|
||||
}
|
||||
|
||||
// NewRegexDomainFilter returns a new DomainFilter given a regular expression
|
||||
func NewRegexDomainFilter(regexDomainFilter *regexp.Regexp, regexDomainExclusion *regexp.Regexp) DomainFilter {
|
||||
return DomainFilter{regex: regexDomainFilter, regexExclusion: regexDomainExclusion}
|
||||
}
|
||||
|
||||
// Match checks whether a domain can be found in the DomainFilter.
|
||||
// RegexFilter takes precedence over Filters
|
||||
func (df DomainFilter) Match(domain string) bool {
|
||||
if df.regex != nil && df.regex.String() != "" {
|
||||
return matchRegex(df.regex, df.regexExclusion, domain)
|
||||
}
|
||||
|
||||
return matchFilter(df.Filters, domain, true) && !matchFilter(df.exclude, domain, false)
|
||||
}
|
||||
|
||||
@ -78,9 +131,49 @@ func matchFilter(filters []string, domain string, emptyval bool) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// matchRegex determines if a domain matches the configured regular expressions in DomainFilter.
|
||||
// negativeRegex, if set, takes precedence over regex. Therefore, matchRegex returns true when
|
||||
// only regex regular expression matches the domain
|
||||
// Otherwise, if either negativeRegex matches or regex does not match the domain, it returns false
|
||||
func matchRegex(regex *regexp.Regexp, negativeRegex *regexp.Regexp, domain string) bool {
|
||||
strippedDomain := strings.ToLower(strings.TrimSuffix(domain, "."))
|
||||
|
||||
if negativeRegex != nil && negativeRegex.String() != "" {
|
||||
return !negativeRegex.MatchString(strippedDomain)
|
||||
}
|
||||
return regex.MatchString(strippedDomain)
|
||||
}
|
||||
|
||||
// MatchParent checks wether DomainFilter matches a given parent domain.
|
||||
func (df DomainFilter) MatchParent(domain string) bool {
|
||||
if !df.IsConfigured() {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, filter := range df.Filters {
|
||||
if strings.HasPrefix(filter, ".") {
|
||||
// We don't check parents if the filter is prefixed with "."
|
||||
continue
|
||||
}
|
||||
|
||||
if filter == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
strippedDomain := strings.ToLower(strings.TrimSuffix(domain, "."))
|
||||
if strings.HasSuffix(filter, "."+strippedDomain) && !matchFilter(df.exclude, domain, false) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// IsConfigured returns true if DomainFilter is configured, false otherwise
|
||||
func (df DomainFilter) IsConfigured() bool {
|
||||
if len(df.Filters) == 1 {
|
||||
if df.regex != nil && df.regex.String() != "" {
|
||||
return true
|
||||
} else if len(df.Filters) == 1 {
|
||||
return df.Filters[0] != ""
|
||||
}
|
||||
return len(df.Filters) > 0
|
||||
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -29,124 +30,131 @@ type domainFilterTest struct {
|
||||
expected bool
|
||||
}
|
||||
|
||||
type regexDomainFilterTest struct {
|
||||
regex *regexp.Regexp
|
||||
regexExclusion *regexp.Regexp
|
||||
domains []string
|
||||
expected bool
|
||||
}
|
||||
|
||||
var domainFilterTests = []domainFilterTest{
|
||||
{
|
||||
[]string{"google.com.", "exaring.de", "inovex.de"},
|
||||
[]string{""},
|
||||
[]string{},
|
||||
[]string{"google.com", "exaring.de", "inovex.de"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]string{"google.com.", "exaring.de", "inovex.de"},
|
||||
[]string{""},
|
||||
[]string{},
|
||||
[]string{"google.com", "exaring.de", "inovex.de"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]string{"google.com.", "exaring.de.", "inovex.de"},
|
||||
[]string{""},
|
||||
[]string{},
|
||||
[]string{"google.com", "exaring.de", "inovex.de"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]string{"foo.org. "},
|
||||
[]string{""},
|
||||
[]string{},
|
||||
[]string{"foo.org"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]string{" foo.org"},
|
||||
[]string{""},
|
||||
[]string{},
|
||||
[]string{"foo.org"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]string{"foo.org."},
|
||||
[]string{""},
|
||||
[]string{},
|
||||
[]string{"foo.org"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]string{"foo.org."},
|
||||
[]string{""},
|
||||
[]string{},
|
||||
[]string{"baz.org"},
|
||||
false,
|
||||
},
|
||||
{
|
||||
[]string{"baz.foo.org."},
|
||||
[]string{""},
|
||||
[]string{},
|
||||
[]string{"foo.org"},
|
||||
false,
|
||||
},
|
||||
{
|
||||
[]string{"", "foo.org."},
|
||||
[]string{""},
|
||||
[]string{},
|
||||
[]string{"foo.org"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]string{"", "foo.org."},
|
||||
[]string{""},
|
||||
[]string{},
|
||||
[]string{},
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]string{""},
|
||||
[]string{""},
|
||||
[]string{},
|
||||
[]string{"foo.org"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]string{""},
|
||||
[]string{""},
|
||||
[]string{},
|
||||
[]string{},
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]string{" "},
|
||||
[]string{""},
|
||||
[]string{},
|
||||
[]string{},
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]string{"bar.sub.example.org"},
|
||||
[]string{""},
|
||||
[]string{},
|
||||
[]string{"foo.bar.sub.example.org"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]string{"example.org"},
|
||||
[]string{""},
|
||||
[]string{},
|
||||
[]string{"anexample.org", "test.anexample.org"},
|
||||
false,
|
||||
},
|
||||
{
|
||||
[]string{".example.org"},
|
||||
[]string{""},
|
||||
[]string{},
|
||||
[]string{"anexample.org", "test.anexample.org"},
|
||||
false,
|
||||
},
|
||||
{
|
||||
[]string{".example.org"},
|
||||
[]string{""},
|
||||
[]string{},
|
||||
[]string{"example.org"},
|
||||
false,
|
||||
},
|
||||
{
|
||||
[]string{".example.org"},
|
||||
[]string{""},
|
||||
[]string{},
|
||||
[]string{"test.example.org"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]string{"anexample.org"},
|
||||
[]string{""},
|
||||
[]string{},
|
||||
[]string{"example.org", "test.example.org"},
|
||||
false,
|
||||
},
|
||||
{
|
||||
[]string{".org"},
|
||||
[]string{""},
|
||||
[]string{},
|
||||
[]string{"example.org", "test.example.org", "foo.test.example.org"},
|
||||
true,
|
||||
},
|
||||
@ -212,10 +220,50 @@ var domainFilterTests = []domainFilterTest{
|
||||
},
|
||||
}
|
||||
|
||||
var regexDomainFilterTests = []regexDomainFilterTest{
|
||||
{
|
||||
regexp.MustCompile("\\.org$"),
|
||||
regexp.MustCompile(""),
|
||||
[]string{"foo.org", "bar.org", "foo.bar.org"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
regexp.MustCompile("\\.bar\\.org$"),
|
||||
regexp.MustCompile(""),
|
||||
[]string{"foo.org", "bar.org", "example.com"},
|
||||
false,
|
||||
},
|
||||
{
|
||||
regexp.MustCompile("(?:foo|bar)\\.org$"),
|
||||
regexp.MustCompile(""),
|
||||
[]string{"foo.org", "bar.org", "example.foo.org", "example.bar.org", "a.example.foo.org", "a.example.bar.org"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
regexp.MustCompile("(?:foo|bar)\\.org$"),
|
||||
regexp.MustCompile("^example\\.(?:foo|bar)\\.org$"),
|
||||
[]string{"foo.org", "bar.org", "a.example.foo.org", "a.example.bar.org"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
regexp.MustCompile("(?:foo|bar)\\.org$"),
|
||||
regexp.MustCompile("^example\\.(?:foo|bar)\\.org$"),
|
||||
[]string{"example.foo.org", "example.bar.org"},
|
||||
false,
|
||||
},
|
||||
{
|
||||
regexp.MustCompile("(?:foo|bar)\\.org$"),
|
||||
regexp.MustCompile("^example\\.(?:foo|bar)\\.org$"),
|
||||
[]string{"foo.org", "bar.org", "a.example.foo.org", "a.example.bar.org"},
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
func TestDomainFilterMatch(t *testing.T) {
|
||||
for i, tt := range domainFilterTests {
|
||||
if len(tt.exclusions) > 0 {
|
||||
t.Skip("NewDomainFilter() doesn't support exclusions")
|
||||
t.Logf("NewDomainFilter() doesn't support exclusions - skipping test %+v", tt)
|
||||
continue
|
||||
}
|
||||
domainFilter := NewDomainFilter(tt.domainFilter)
|
||||
for _, domain := range tt.domains {
|
||||
@ -227,6 +275,9 @@ func TestDomainFilterMatch(t *testing.T) {
|
||||
|
||||
func TestDomainFilterWithExclusions(t *testing.T) {
|
||||
for i, tt := range domainFilterTests {
|
||||
if len(tt.exclusions) == 0 {
|
||||
tt.exclusions = append(tt.exclusions, "")
|
||||
}
|
||||
domainFilter := NewDomainFilterWithExclusions(tt.domainFilter, tt.exclusions)
|
||||
for _, domain := range tt.domains {
|
||||
assert.Equal(t, tt.expected, domainFilter.Match(domain), "should not fail: %v in test-case #%v", domain, i)
|
||||
@ -245,11 +296,91 @@ func TestDomainFilterMatchWithEmptyFilter(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDomainFilterMatchParent(t *testing.T) {
|
||||
parentMatchTests := []domainFilterTest{
|
||||
{
|
||||
[]string{"a.example.com."},
|
||||
[]string{},
|
||||
[]string{"example.com"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]string{" a.example.com "},
|
||||
[]string{},
|
||||
[]string{"example.com"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]string{""},
|
||||
[]string{},
|
||||
[]string{"example.com"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]string{".a.example.com."},
|
||||
[]string{},
|
||||
[]string{"example.com"},
|
||||
false,
|
||||
},
|
||||
{
|
||||
[]string{"a.example.com.", "b.example.com"},
|
||||
[]string{},
|
||||
[]string{"example.com"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]string{"a.example.com"},
|
||||
[]string{},
|
||||
[]string{"b.example.com"},
|
||||
false,
|
||||
},
|
||||
{
|
||||
[]string{"example.com"},
|
||||
[]string{},
|
||||
[]string{"example.com"},
|
||||
false,
|
||||
},
|
||||
{
|
||||
[]string{"example.com"},
|
||||
[]string{},
|
||||
[]string{"anexample.com"},
|
||||
false,
|
||||
},
|
||||
{
|
||||
[]string{""},
|
||||
[]string{},
|
||||
[]string{""},
|
||||
true,
|
||||
},
|
||||
}
|
||||
for i, tt := range parentMatchTests {
|
||||
domainFilter := NewDomainFilterWithExclusions(tt.domainFilter, tt.exclusions)
|
||||
for _, domain := range tt.domains {
|
||||
assert.Equal(t, tt.expected, domainFilter.MatchParent(domain), "should not fail: %v in test-case #%v", domain, i)
|
||||
assert.Equal(t, tt.expected, domainFilter.MatchParent(domain+"."), "should not fail: %v in test-case #%v", domain+".", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegexDomainFilter(t *testing.T) {
|
||||
for i, tt := range regexDomainFilterTests {
|
||||
domainFilter := NewRegexDomainFilter(tt.regex, tt.regexExclusion)
|
||||
for _, domain := range tt.domains {
|
||||
assert.Equal(t, tt.expected, domainFilter.Match(domain), "should not fail: %v in test-case #%v", domain, i)
|
||||
assert.Equal(t, tt.expected, domainFilter.Match(domain+"."), "should not fail: %v in test-case #%v", domain+".", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareFiltersStripsWhitespaceAndDotSuffix(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
input []string
|
||||
output []string
|
||||
}{
|
||||
{
|
||||
[]string{},
|
||||
[]string{},
|
||||
},
|
||||
{
|
||||
[]string{""},
|
||||
[]string{""},
|
||||
|
@ -71,7 +71,7 @@ func (t Targets) Swap(i, j int) {
|
||||
t[i], t[j] = t[j], t[i]
|
||||
}
|
||||
|
||||
// Same compares to Targets and returns true if they are completely identical
|
||||
// Same compares to Targets and returns true if they are identical (case-insensitive)
|
||||
func (t Targets) Same(o Targets) bool {
|
||||
if len(t) != len(o) {
|
||||
return false
|
||||
@ -80,7 +80,7 @@ func (t Targets) Same(o Targets) bool {
|
||||
sort.Stable(o)
|
||||
|
||||
for i, e := range t {
|
||||
if e != o[i] {
|
||||
if !strings.EqualFold(e, o[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@ -213,8 +213,12 @@ type DNSEndpointStatus struct {
|
||||
// DNSEndpoint is a contract that a user-specified CRD must implement to be used as a source for external-dns.
|
||||
// The user-specified CRD should also have the status sub-resource.
|
||||
// +k8s:openapi-gen=true
|
||||
// +groupName=externaldns.k8s.io
|
||||
// +kubebuilder:resource:path=dnsendpoints
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:subresource:status
|
||||
// +versionName=v1alpha1
|
||||
|
||||
type DNSEndpoint struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
@ -223,6 +227,7 @@ type DNSEndpoint struct {
|
||||
Status DNSEndpointStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// DNSEndpointList is a list of DNSEndpoint objects
|
||||
type DNSEndpointList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
|
@ -40,6 +40,7 @@ func TestTargetsSame(t *testing.T) {
|
||||
{""},
|
||||
{"1.2.3.4"},
|
||||
{"8.8.8.8", "8.8.4.4"},
|
||||
{"example.org", "EXAMPLE.ORG"},
|
||||
}
|
||||
|
||||
for _, d := range tests {
|
||||
|
25
go.mod
25
go.mod
@ -1,6 +1,6 @@
|
||||
module sigs.k8s.io/external-dns
|
||||
|
||||
go 1.15
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.50.0
|
||||
@ -16,6 +16,7 @@ require (
|
||||
github.com/alecthomas/repr v0.0.0-20200325044227-4184120f674c // indirect
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.357
|
||||
github.com/aws/aws-sdk-go v1.31.4
|
||||
github.com/bodgit/tsig v0.0.2
|
||||
github.com/cloudflare/cloudflare-go v0.10.1
|
||||
github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381
|
||||
github.com/datawire/ambassador v1.6.0
|
||||
@ -25,15 +26,17 @@ require (
|
||||
github.com/exoscale/egoscale v0.18.1
|
||||
github.com/fatih/structs v1.1.0 // indirect
|
||||
github.com/ffledgling/pdns-go v0.0.0-20180219074714-524e7daccd99
|
||||
github.com/go-gandi/go-gandi v0.0.0-20200921091836-0d8a64b9cc09
|
||||
github.com/golang/sync v0.0.0-20180314180146-1d60e4601c6f
|
||||
github.com/google/go-cmp v0.4.1
|
||||
github.com/google/go-cmp v0.5.2
|
||||
github.com/gophercloud/gophercloud v0.1.0
|
||||
github.com/gorilla/mux v1.7.4 // indirect
|
||||
github.com/infobloxopen/infoblox-go-client v0.0.0-20180606155407-61dc5f9b0a65
|
||||
github.com/hooklift/gowsdl v0.4.0
|
||||
github.com/infobloxopen/infoblox-go-client v1.1.1
|
||||
github.com/linki/instrumented_http v0.2.0
|
||||
github.com/linode/linodego v0.19.0
|
||||
github.com/maxatome/go-testdeep v1.4.0
|
||||
github.com/miekg/dns v1.1.30
|
||||
github.com/miekg/dns v1.1.36-0.20210109083720-731b191cabd1
|
||||
github.com/nesv/go-dynect v0.6.0
|
||||
github.com/nic-at/rc0go v1.1.1
|
||||
github.com/openshift/api v0.0.0-20200605231317-fb2a6ca106ae
|
||||
@ -43,27 +46,27 @@ require (
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/projectcontour/contour v1.5.0
|
||||
github.com/prometheus/client_golang v1.7.1
|
||||
github.com/sanyu/dynectsoap v0.0.0-20181203081243-b83de5edc4e0
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.6.0.20200623155123-84df6c4b5301
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.7.0.20210127161313-bd30bebeac4f
|
||||
github.com/sirupsen/logrus v1.6.0
|
||||
github.com/smartystreets/gunit v1.3.4 // indirect
|
||||
github.com/stretchr/testify v1.6.1
|
||||
github.com/terra-farm/udnssdk v1.3.5 // indirect
|
||||
github.com/transip/gotransip v5.8.2+incompatible
|
||||
github.com/transip/gotransip/v6 v6.6.0
|
||||
github.com/ultradns/ultradns-sdk-go v0.0.0-20200616202852-e62052662f60
|
||||
github.com/vinyldns/go-vinyldns v0.0.0-20200211145900-fe8a3d82e556
|
||||
github.com/vultr/govultr v0.4.2
|
||||
github.com/vultr/govultr/v2 v2.5.1
|
||||
go.etcd.io/etcd v0.5.0-alpha.5.0.20200401174654-e694b7bb0875
|
||||
go.uber.org/ratelimit v0.1.0
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208
|
||||
golang.org/x/tools v0.0.0-20200708003708-134513de8882 // indirect
|
||||
google.golang.org/api v0.15.0
|
||||
gopkg.in/ns1/ns1-go.v2 v2.0.0-20190322154155-0dafb5275fd1
|
||||
gopkg.in/yaml.v2 v2.3.0
|
||||
honnef.co/go/tools v0.0.1-2020.1.4 // indirect
|
||||
istio.io/api v0.0.0-20200529165953-72dad51d4ffc
|
||||
istio.io/client-go v0.0.0-20200529172309-31c16ea3f751
|
||||
istio.io/api v0.0.0-20210128181506-0c4b8e54850f
|
||||
istio.io/client-go v0.0.0-20210128182905-ee2edd059e02
|
||||
k8s.io/api v0.18.8
|
||||
k8s.io/apimachinery v0.18.8
|
||||
k8s.io/client-go v0.18.8
|
||||
|
85
go.sum
85
go.sum
@ -84,6 +84,7 @@ github.com/alecthomas/colour v0.1.0 h1:nOE9rJm6dsZ66RGWYSFrXw461ZIt9A6+nHgL7FRrD
|
||||
github.com/alecthomas/colour v0.1.0/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
|
||||
github.com/alecthomas/kingpin v2.2.5+incompatible h1:umWl1NNd72+ZvRti3T9C0SYean2hPZ7ZhxU8bsgc9BQ=
|
||||
github.com/alecthomas/kingpin v2.2.5+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE=
|
||||
github.com/alecthomas/kong v0.2.2/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE=
|
||||
github.com/alecthomas/repr v0.0.0-20200325044227-4184120f674c h1:MVVbswUlqicyj8P/JljoocA7AyCo62gzD0O7jfvrhtE=
|
||||
github.com/alecthomas/repr v0.0.0-20200325044227-4184120f674c/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
@ -92,6 +93,8 @@ 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=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||
@ -124,6 +127,8 @@ github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngE
|
||||
github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||
github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||
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/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=
|
||||
@ -231,6 +236,7 @@ github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkg
|
||||
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
|
||||
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
|
||||
github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
|
||||
github.com/enceve/crypto v0.0.0-20160707101852-34d48bb93815/go.mod h1:wYFFK4LYXbX7j+76mOq7aiC/EAw2S22CrzPHqgsisPw=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.5/go.mod h1:OXl5to++W0ctG+EHWTFUjiypVxC/Y4VLc/KFU+al13s=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
@ -261,6 +267,8 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME
|
||||
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
|
||||
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-gandi/go-gandi v0.0.0-20200921091836-0d8a64b9cc09 h1:w+iZczt5J4LJa13RX5uguKI866vIEMOESgXr4XcwrwA=
|
||||
github.com/go-gandi/go-gandi v0.0.0-20200921091836-0d8a64b9cc09/go.mod h1:Vv36tv/GTi8FNAFIQ88+9GPHm4CAihAuJu7rfqRJ9aY=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
@ -385,10 +393,11 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.4.1 h1:/exdXoGamhu5ONeUJH0deniYLWYvQwW66yvlfiiKTu0=
|
||||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
|
||||
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g=
|
||||
@ -421,6 +430,10 @@ 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=
|
||||
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
|
||||
@ -443,6 +456,7 @@ github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplb
|
||||
github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
|
||||
github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
||||
github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
@ -452,13 +466,17 @@ github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjh
|
||||
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||
github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/go-retryablehttp v0.6.6 h1:HJunrbHTDDbBb/ay4kxa1n+dLmttUlnP3V9oNE4hmsM=
|
||||
github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
|
||||
github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI=
|
||||
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.0 h1:eu1EI/mbirUgP5C8hVsTNaGZreBDlYiwC1FZWkvQPQ4=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.0/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
|
||||
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE=
|
||||
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
@ -470,6 +488,8 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO
|
||||
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
||||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
github.com/hooklift/gowsdl v0.4.0 h1:luskQG8h3M0CYrcSFl9ObpWs3pzIsEfYou1cuSwKiCk=
|
||||
github.com/hooklift/gowsdl v0.4.0/go.mod h1:TYmt7jpe3F5zLlMtKGetjHLwUBIAF5JCd+NYq+mQ/Zk=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
|
||||
@ -482,8 +502,20 @@ github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg=
|
||||
github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
|
||||
github.com/infobloxopen/infoblox-go-client v0.0.0-20180606155407-61dc5f9b0a65 h1:FP5rOFP4ifbtFIjFHJmwhFrsbDyONILK/FNntl/Pou8=
|
||||
github.com/infobloxopen/infoblox-go-client v0.0.0-20180606155407-61dc5f9b0a65/go.mod h1:BXiw7S2b9qJoM8MS40vfgCNB2NLHGusk1DtO16BD9zI=
|
||||
github.com/infobloxopen/infoblox-go-client v1.1.1 h1:728A6LbLjptj/7kZjHyIxQnm768PWHfGFm0HH8FnbtU=
|
||||
github.com/infobloxopen/infoblox-go-client v1.1.1/go.mod h1:BXiw7S2b9qJoM8MS40vfgCNB2NLHGusk1DtO16BD9zI=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
|
||||
github.com/jcmturner/gofork v1.0.0 h1:J7uCkflzTEhUZ64xqKnkDxq3kzc96ajM1Gli5ktUem8=
|
||||
github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
|
||||
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
|
||||
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.1 h1:IGSJfqBzMS6TA0oJ7DxXdyzPK563QHa8T2IqER2ggyQ=
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.1/go.mod h1:T1hnNppQsBtxW0tCHMHTkAt8n/sABdzZgZdoFrZaZNM=
|
||||
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/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=
|
||||
@ -572,8 +604,9 @@ 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=
|
||||
github.com/mikkeloscar/knolog v0.0.0-20190326191552-80742771eb6b h1:5f5B1kp+QerGOF91q1qVJcUWWvXsVEN3OKiyEzAAjIM=
|
||||
github.com/mikkeloscar/knolog v0.0.0-20190326191552-80742771eb6b/go.mod h1:PizLs/1ddmVrXpFgWOGNmTJ2YHSWUkpUXMYuUkTo3Go=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
@ -610,7 +643,6 @@ github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzE
|
||||
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
|
||||
github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
|
||||
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
|
||||
github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM=
|
||||
github.com/nesv/go-dynect v0.6.0 h1:Ow/DiSm4LAISwnFku/FITSQHnU6pBvhQMsUE5Gu6Oq4=
|
||||
@ -660,6 +692,8 @@ 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=
|
||||
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
@ -755,11 +789,9 @@ github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
|
||||
github.com/sanyu/dynectsoap v0.0.0-20181203081243-b83de5edc4e0 h1:vOcHdR1nu7DO4BAx1rwzdHV7jQTzW3gqcBT5qxHSc6A=
|
||||
github.com/sanyu/dynectsoap v0.0.0-20181203081243-b83de5edc4e0/go.mod h1:FeplEtXXejBYC4NPAFTrs5L7KuK+5RL9bf5nB2vZe9o=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.6.0.20200623155123-84df6c4b5301 h1:qj0du14RIOnmePII/eTlw1aHKDYL6zxDIk/Dq7Tef9k=
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.6.0.20200623155123-84df6c4b5301/go.mod h1:CJJ5VAbozOl0yEw7nHB9+7BXTJbIn6h7W+f6Gau5IP8=
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.7.0.20210127161313-bd30bebeac4f h1:WSnaD0/cvbKJgSTYbjAPf4RJXVvNNDAwVm+W8wEmnGE=
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.7.0.20210127161313-bd30bebeac4f/go.mod h1:CJJ5VAbozOl0yEw7nHB9+7BXTJbIn6h7W+f6Gau5IP8=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||
@ -827,8 +859,8 @@ github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhV
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 h1:LnC5Kc/wtumK+WB441p7ynQJzVuNRJiqddSIE3IlSEQ=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/transip/gotransip v5.8.2+incompatible h1:aNJhw/w/3QBqFcHAIPz1ytoK5FexeMzbUCGrrhWr3H0=
|
||||
github.com/transip/gotransip v5.8.2+incompatible/go.mod h1:uacMoJVmrfOcscM4Bi5NVg708b7c6rz2oDTWqa7i2Ic=
|
||||
github.com/transip/gotransip/v6 v6.6.0 h1:dAHCTZzX98H6QE2kA4R9acAXu5RPPTwMSUFtpKZF3Nk=
|
||||
github.com/transip/gotransip/v6 v6.6.0/go.mod h1:pQZ36hWWRahCUXkFWlx9Hs711gLd8J4qdgLdRzmtY+g=
|
||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
|
||||
@ -841,8 +873,8 @@ github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtX
|
||||
github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw=
|
||||
github.com/vinyldns/go-vinyldns v0.0.0-20200211145900-fe8a3d82e556 h1:UbVjBjgJUYGD8MlobEdOR+yTeNqaNa2Gf1/nskVNCSE=
|
||||
github.com/vinyldns/go-vinyldns v0.0.0-20200211145900-fe8a3d82e556/go.mod h1:RWc47jtnVuQv6+lY3c768WtXCas/Xi+U5UFc5xULmYg=
|
||||
github.com/vultr/govultr v0.4.2 h1:9i8xKZ+xp6vwZ9raqHoBLzhB4wCnMj7nOQTj5YIRLWY=
|
||||
github.com/vultr/govultr v0.4.2/go.mod h1:TUuUizMOFc7z+PNMssb6iGjKjQfpw5arIaOLfocVudQ=
|
||||
github.com/vultr/govultr/v2 v2.5.1 h1:Bh3G7nqHs0Gv7OQRExfYFppbuscwVKFDK05b8XBYYnQ=
|
||||
github.com/vultr/govultr/v2 v2.5.1/go.mod h1:BvOhVe6/ZpjwcoL6/unkdQshmbS9VGbowI4QT+3DGVU=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs=
|
||||
@ -910,6 +942,7 @@ golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
@ -971,12 +1004,14 @@ golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b h1:iFwSg7t5GZmB/Q5TjiEAsdoLDrdJRC1RiF2WhuV29Qw=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190130055435-99b60b757ec1/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@ -1031,8 +1066,10 @@ golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1 h1:ogLJMz+qpzav7lGMh10LMvAkM/fAoGlaiiHYiFYdm80=
|
||||
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/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=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20171227012246-e19ae1496984/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@ -1180,10 +1217,10 @@ 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/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/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-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=
|
||||
istio.io/gogo-genproto v0.0.0-20190930162913-45029607206a h1:w7zILua2dnYo9CxImhpNW4NE/8ZxEoc/wfBfHrhUhrE=
|
||||
istio.io/gogo-genproto v0.0.0-20190930162913-45029607206a/go.mod h1:OzpAts7jljZceG4Vqi5/zXy/pOg1b209T3jb7Nv5wIs=
|
||||
|
@ -25,6 +25,12 @@ import (
|
||||
|
||||
/** test utility functions for endpoints verifications */
|
||||
|
||||
type byNames endpoint.ProviderSpecific
|
||||
|
||||
func (p byNames) Len() int { return len(p) }
|
||||
func (p byNames) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
||||
func (p byNames) Less(i, j int) bool { return p[i].Name < p[j].Name }
|
||||
|
||||
type byAllFields []*endpoint.Endpoint
|
||||
|
||||
func (b byAllFields) Len() int { return len(b) }
|
||||
@ -102,5 +108,9 @@ func SamePlanChanges(a, b map[string][]*endpoint.Endpoint) bool {
|
||||
|
||||
// SameProviderSpecific verifies that two maps contain the same string/string key/value pairs
|
||||
func SameProviderSpecific(a, b endpoint.ProviderSpecific) bool {
|
||||
return reflect.DeepEqual(a, b)
|
||||
sa := a
|
||||
sb := b
|
||||
sort.Sort(byNames(sa))
|
||||
sort.Sort(byNames(sb))
|
||||
return reflect.DeepEqual(sa, sb)
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: external-dns
|
||||
|
@ -1,4 +1,4 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: external-dns-viewer
|
||||
|
@ -3,7 +3,7 @@ kind: Kustomization
|
||||
|
||||
images:
|
||||
- name: k8s.gcr.io/external-dns/external-dns
|
||||
newTag: v0.7.6
|
||||
newTag: v0.8.0
|
||||
|
||||
resources:
|
||||
- ./external-dns-deployment.yaml
|
||||
|
28
main.go
28
main.go
@ -40,6 +40,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"
|
||||
@ -47,6 +48,8 @@ import (
|
||||
"sigs.k8s.io/external-dns/provider/dnsimple"
|
||||
"sigs.k8s.io/external-dns/provider/dyn"
|
||||
"sigs.k8s.io/external-dns/provider/exoscale"
|
||||
"sigs.k8s.io/external-dns/provider/gandi"
|
||||
"sigs.k8s.io/external-dns/provider/godaddy"
|
||||
"sigs.k8s.io/external-dns/provider/google"
|
||||
"sigs.k8s.io/external-dns/provider/hetzner"
|
||||
"sigs.k8s.io/external-dns/provider/infoblox"
|
||||
@ -106,6 +109,7 @@ func main() {
|
||||
CombineFQDNAndAnnotation: cfg.CombineFQDNAndAnnotation,
|
||||
IgnoreHostnameAnnotation: cfg.IgnoreHostnameAnnotation,
|
||||
IgnoreIngressTLSSpec: cfg.IgnoreIngressTLSSpec,
|
||||
IgnoreIngressRulesSpec: cfg.IgnoreIngressRulesSpec,
|
||||
Compatibility: cfg.Compatibility,
|
||||
PublishInternal: cfg.PublishInternal,
|
||||
PublishHostIP: cfg.PublishHostIP,
|
||||
@ -120,8 +124,10 @@ func main() {
|
||||
CFUsername: cfg.CFUsername,
|
||||
CFPassword: cfg.CFPassword,
|
||||
ContourLoadBalancerService: cfg.ContourLoadBalancerService,
|
||||
GlooNamespace: cfg.GlooNamespace,
|
||||
SkipperRouteGroupVersion: cfg.SkipperRouteGroupVersion,
|
||||
RequestTimeout: cfg.RequestTimeout,
|
||||
DefaultTargets: cfg.DefaultTargets,
|
||||
}
|
||||
|
||||
// Lookup all the selected sources by names and pass them the desired configuration.
|
||||
@ -141,9 +147,15 @@ func main() {
|
||||
}
|
||||
|
||||
// Combine multiple sources into a single, deduplicated source.
|
||||
endpointsSource := source.NewDedupSource(source.NewMultiSource(sources))
|
||||
endpointsSource := source.NewDedupSource(source.NewMultiSource(sources, sourceCfg.DefaultTargets))
|
||||
|
||||
domainFilter := endpoint.NewDomainFilterWithExclusions(cfg.DomainFilter, cfg.ExcludeDomains)
|
||||
// RegexDomainFilter overrides DomainFilter
|
||||
var domainFilter endpoint.DomainFilter
|
||||
if cfg.RegexDomainFilter.String() != "" {
|
||||
domainFilter = endpoint.NewRegexDomainFilter(cfg.RegexDomainFilter, cfg.RegexDomainExclusion)
|
||||
} else {
|
||||
domainFilter = endpoint.NewDomainFilterWithExclusions(cfg.DomainFilter, cfg.ExcludeDomains)
|
||||
}
|
||||
zoneNameFilter := endpoint.NewDomainFilter(cfg.ZoneNameFilter)
|
||||
zoneIDFilter := provider.NewZoneIDFilter(cfg.ZoneIDFilter)
|
||||
zoneTypeFilter := provider.NewZoneTypeFilter(cfg.AWSZoneType)
|
||||
@ -194,10 +206,12 @@ 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":
|
||||
p, err = vultr.NewVultrProvider(domainFilter, cfg.DryRun)
|
||||
p, err = vultr.NewVultrProvider(ctx, domainFilter, cfg.DryRun)
|
||||
case "ultradns":
|
||||
p, err = ultradns.NewUltraDNSProvider(domainFilter, cfg.DryRun)
|
||||
case "cloudflare":
|
||||
@ -230,6 +244,7 @@ func main() {
|
||||
View: cfg.InfobloxView,
|
||||
MaxResults: cfg.InfobloxMaxResults,
|
||||
DryRun: cfg.DryRun,
|
||||
FQDNRexEx: cfg.InfobloxFQDNRegEx,
|
||||
},
|
||||
)
|
||||
case "dyn":
|
||||
@ -295,7 +310,7 @@ func main() {
|
||||
p, err = oci.NewOCIProvider(*config, domainFilter, zoneIDFilter, cfg.DryRun)
|
||||
}
|
||||
case "rfc2136":
|
||||
p, err = rfc2136.NewRfc2136Provider(cfg.RFC2136Host, cfg.RFC2136Port, cfg.RFC2136Zone, cfg.RFC2136Insecure, cfg.RFC2136TSIGKeyName, cfg.RFC2136TSIGSecret, cfg.RFC2136TSIGSecretAlg, cfg.RFC2136TAXFR, domainFilter, cfg.DryRun, cfg.RFC2136MinTTL, nil)
|
||||
p, err = rfc2136.NewRfc2136Provider(cfg.RFC2136Host, cfg.RFC2136Port, cfg.RFC2136Zone, cfg.RFC2136Insecure, cfg.RFC2136TSIGKeyName, cfg.RFC2136TSIGSecret, cfg.RFC2136TSIGSecretAlg, cfg.RFC2136TAXFR, domainFilter, cfg.DryRun, cfg.RFC2136MinTTL, cfg.RFC2136GSSTSIG, cfg.RFC2136KerberosUsername, cfg.RFC2136KerberosPassword, cfg.RFC2136KerberosRealm, cfg.RFC2136BatchChangeSize, nil)
|
||||
case "ns1":
|
||||
p, err = ns1.NewNS1Provider(
|
||||
ns1.NS1Config{
|
||||
@ -311,6 +326,10 @@ func main() {
|
||||
p, err = transip.NewTransIPProvider(cfg.TransIPAccountName, cfg.TransIPPrivateKeyFile, domainFilter, cfg.DryRun)
|
||||
case "scaleway":
|
||||
p, err = scaleway.NewScalewayProvider(ctx, domainFilter, cfg.DryRun)
|
||||
case "godaddy":
|
||||
p, err = godaddy.NewGoDaddyProvider(ctx, domainFilter, cfg.GoDaddyTTL, cfg.GoDaddyAPIKey, cfg.GoDaddySecretKey, cfg.GoDaddyOTE, cfg.DryRun)
|
||||
case "gandi":
|
||||
p, err = gandi.NewGandiProvider(ctx, domainFilter, cfg.DryRun)
|
||||
default:
|
||||
log.Fatalf("unknown dns provider: %s", cfg.Provider)
|
||||
}
|
||||
@ -346,6 +365,7 @@ func main() {
|
||||
Interval: cfg.Interval,
|
||||
DomainFilter: domainFilter,
|
||||
ManagedRecordTypes: cfg.ManagedDNSRecordTypes,
|
||||
MinEventSyncInterval: cfg.MinEventSyncInterval,
|
||||
}
|
||||
|
||||
if cfg.Once {
|
||||
|
@ -19,6 +19,7 @@ package externaldns
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
@ -44,7 +45,9 @@ type Config struct {
|
||||
APIServerURL string
|
||||
KubeConfig string
|
||||
RequestTimeout time.Duration
|
||||
DefaultTargets []string
|
||||
ContourLoadBalancerService string
|
||||
GlooNamespace string
|
||||
SkipperRouteGroupVersion string
|
||||
Sources []string
|
||||
Namespace string
|
||||
@ -54,6 +57,7 @@ type Config struct {
|
||||
CombineFQDNAndAnnotation bool
|
||||
IgnoreHostnameAnnotation bool
|
||||
IgnoreIngressTLSSpec bool
|
||||
IgnoreIngressRulesSpec bool
|
||||
Compatibility string
|
||||
PublishInternal bool
|
||||
PublishHostIP bool
|
||||
@ -65,6 +69,8 @@ type Config struct {
|
||||
GoogleBatchChangeInterval time.Duration
|
||||
DomainFilter []string
|
||||
ExcludeDomains []string
|
||||
RegexDomainFilter *regexp.Regexp
|
||||
RegexDomainExclusion *regexp.Regexp
|
||||
ZoneNameFilter []string
|
||||
ZoneIDFilter []string
|
||||
AlibabaCloudConfigFile string
|
||||
@ -82,6 +88,7 @@ type Config struct {
|
||||
AzureResourceGroup string
|
||||
AzureSubscriptionID string
|
||||
AzureUserAssignedIdentityClientID string
|
||||
BluecatConfigFile string
|
||||
CloudflareProxied bool
|
||||
CloudflareZonesPerPage int
|
||||
CoreDNSPrefix string
|
||||
@ -100,6 +107,7 @@ type Config struct {
|
||||
InfobloxSSLVerify bool
|
||||
InfobloxView string
|
||||
InfobloxMaxResults int
|
||||
InfobloxFQDNRegEx string
|
||||
DynCustomerName string
|
||||
DynUsername string
|
||||
DynPassword string `secure:"yes"`
|
||||
@ -122,6 +130,7 @@ type Config struct {
|
||||
TXTPrefix string
|
||||
TXTSuffix string
|
||||
Interval time.Duration
|
||||
MinEventSyncInterval time.Duration
|
||||
Once bool
|
||||
DryRun bool
|
||||
UpdateEvents bool
|
||||
@ -143,11 +152,16 @@ type Config struct {
|
||||
RFC2136Port int
|
||||
RFC2136Zone string
|
||||
RFC2136Insecure bool
|
||||
RFC2136GSSTSIG bool
|
||||
RFC2136KerberosRealm string
|
||||
RFC2136KerberosUsername string
|
||||
RFC2136KerberosPassword string
|
||||
RFC2136TSIGKeyName string
|
||||
RFC2136TSIGSecret string `secure:"yes"`
|
||||
RFC2136TSIGSecretAlg string
|
||||
RFC2136TAXFR bool
|
||||
RFC2136MinTTL time.Duration
|
||||
RFC2136BatchChangeSize int
|
||||
NS1Endpoint string
|
||||
NS1IgnoreSSL bool
|
||||
NS1MinTTLSeconds int
|
||||
@ -155,13 +169,19 @@ type Config struct {
|
||||
TransIPPrivateKeyFile string
|
||||
DigitalOceanAPIPageSize int
|
||||
ManagedDNSRecordTypes []string
|
||||
GoDaddyAPIKey string `secure:"yes"`
|
||||
GoDaddySecretKey string `secure:"yes"`
|
||||
GoDaddyTTL int64
|
||||
GoDaddyOTE bool
|
||||
}
|
||||
|
||||
var defaultConfig = &Config{
|
||||
APIServerURL: "",
|
||||
KubeConfig: "",
|
||||
RequestTimeout: time.Second * 30,
|
||||
DefaultTargets: []string{},
|
||||
ContourLoadBalancerService: "heptio-contour/contour",
|
||||
GlooNamespace: "gloo-system",
|
||||
SkipperRouteGroupVersion: "zalando.org/v1",
|
||||
Sources: nil,
|
||||
Namespace: "",
|
||||
@ -171,6 +191,7 @@ var defaultConfig = &Config{
|
||||
CombineFQDNAndAnnotation: false,
|
||||
IgnoreHostnameAnnotation: false,
|
||||
IgnoreIngressTLSSpec: false,
|
||||
IgnoreIngressRulesSpec: false,
|
||||
Compatibility: "",
|
||||
PublishInternal: false,
|
||||
PublishHostIP: false,
|
||||
@ -181,6 +202,8 @@ var defaultConfig = &Config{
|
||||
GoogleBatchChangeInterval: time.Second,
|
||||
DomainFilter: []string{},
|
||||
ExcludeDomains: []string{},
|
||||
RegexDomainFilter: regexp.MustCompile(""),
|
||||
RegexDomainExclusion: regexp.MustCompile(""),
|
||||
AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json",
|
||||
AWSZoneType: "",
|
||||
AWSZoneTagFilter: []string{},
|
||||
@ -194,6 +217,7 @@ var defaultConfig = &Config{
|
||||
AzureConfigFile: "/etc/kubernetes/azure.json",
|
||||
AzureResourceGroup: "",
|
||||
AzureSubscriptionID: "",
|
||||
BluecatConfigFile: "/etc/kubernetes/bluecat.json",
|
||||
CloudflareProxied: false,
|
||||
CloudflareZonesPerPage: 50,
|
||||
CoreDNSPrefix: "/skydns/",
|
||||
@ -212,6 +236,7 @@ var defaultConfig = &Config{
|
||||
InfobloxSSLVerify: true,
|
||||
InfobloxView: "",
|
||||
InfobloxMaxResults: 0,
|
||||
InfobloxFQDNRegEx: "",
|
||||
OCIConfigFile: "/etc/kubernetes/oci.yaml",
|
||||
InMemoryZones: []string{},
|
||||
OVHEndpoint: "ovh-eu",
|
||||
@ -229,6 +254,7 @@ var defaultConfig = &Config{
|
||||
TXTSuffix: "",
|
||||
TXTCacheInterval: 0,
|
||||
TXTWildcardReplacement: "",
|
||||
MinEventSyncInterval: 5 * time.Second,
|
||||
Interval: time.Minute,
|
||||
Once: false,
|
||||
DryRun: false,
|
||||
@ -249,17 +275,26 @@ var defaultConfig = &Config{
|
||||
RFC2136Port: 0,
|
||||
RFC2136Zone: "",
|
||||
RFC2136Insecure: false,
|
||||
RFC2136GSSTSIG: false,
|
||||
RFC2136KerberosRealm: "",
|
||||
RFC2136KerberosUsername: "",
|
||||
RFC2136KerberosPassword: "",
|
||||
RFC2136TSIGKeyName: "",
|
||||
RFC2136TSIGSecret: "",
|
||||
RFC2136TSIGSecretAlg: "",
|
||||
RFC2136TAXFR: true,
|
||||
RFC2136MinTTL: 0,
|
||||
RFC2136BatchChangeSize: 50,
|
||||
NS1Endpoint: "",
|
||||
NS1IgnoreSSL: false,
|
||||
TransIPAccountName: "",
|
||||
TransIPPrivateKeyFile: "",
|
||||
DigitalOceanAPIPageSize: 50,
|
||||
ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||
GoDaddyAPIKey: "",
|
||||
GoDaddySecretKey: "",
|
||||
GoDaddyTTL: 600,
|
||||
GoDaddyOTE: false,
|
||||
}
|
||||
|
||||
// NewConfig returns new Config object
|
||||
@ -316,12 +351,14 @@ func (cfg *Config) ParseFlags(args []string) error {
|
||||
// Flags related to Contour
|
||||
app.Flag("contour-load-balancer", "The fully-qualified name of the Contour load balancer service. (default: heptio-contour/contour)").Default("heptio-contour/contour").StringVar(&cfg.ContourLoadBalancerService)
|
||||
|
||||
// Flags related to Gloo
|
||||
app.Flag("gloo-namespace", "Gloo namespace. (default: gloo-system)").Default("gloo-system").StringVar(&cfg.GlooNamespace)
|
||||
|
||||
// Flags related to Skipper RouteGroup
|
||||
app.Flag("skipper-routegroup-groupversion", "The resource version for skipper routegroup").Default(source.DefaultRoutegroupVersion).StringVar(&cfg.SkipperRouteGroupVersion)
|
||||
|
||||
// Flags related to processing sources
|
||||
app.Flag("source", "The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress, node, fake, connector, istio-gateway, istio-virtualservice, cloudfoundry, contour-ingressroute, contour-httpproxy, crd, empty, skipper-routegroup, openshift-route, ambassador-host)").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress", "node", "istio-gateway", "istio-virtualservice", "cloudfoundry", "contour-ingressroute", "contour-httpproxy", "fake", "connector", "crd", "empty", "skipper-routegroup", "openshift-route", "ambassador-host")
|
||||
|
||||
app.Flag("source", "The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress, node, fake, connector, istio-gateway, istio-virtualservice, cloudfoundry, contour-ingressroute, contour-httpproxy, gloo-proxy, crd, empty, skipper-routegroup, openshift-route, ambassador-host, kong-tcpingress)").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress", "node", "pod", "istio-gateway", "istio-virtualservice", "cloudfoundry", "contour-ingressroute", "contour-httpproxy", "gloo-proxy", "fake", "connector", "crd", "empty", "skipper-routegroup", "openshift-route", "ambassador-host", "kong-tcpingress")
|
||||
app.Flag("namespace", "Limit sources of endpoints to a specific namespace (default: all namespaces)").Default(defaultConfig.Namespace).StringVar(&cfg.Namespace)
|
||||
app.Flag("annotation-filter", "Filter sources managed by external-dns via annotation using label selector semantics (default: all sources)").Default(defaultConfig.AnnotationFilter).StringVar(&cfg.AnnotationFilter)
|
||||
app.Flag("label-filter", "Filter sources managed by external-dns via label selector when listing all resources; currently only supported by source CRD").Default(defaultConfig.LabelFilter).StringVar(&cfg.LabelFilter)
|
||||
@ -329,7 +366,8 @@ func (cfg *Config) ParseFlags(args []string) error {
|
||||
app.Flag("combine-fqdn-annotation", "Combine FQDN template and Annotations instead of overwriting").BoolVar(&cfg.CombineFQDNAndAnnotation)
|
||||
app.Flag("ignore-hostname-annotation", "Ignore hostname annotation when generating DNS names, valid only when using fqdn-template is set (optional, default: false)").BoolVar(&cfg.IgnoreHostnameAnnotation)
|
||||
app.Flag("ignore-ingress-tls-spec", "Ignore tls spec section in ingresses resources, applicable only for ingress sources (optional, default: false)").BoolVar(&cfg.IgnoreIngressTLSSpec)
|
||||
app.Flag("compatibility", "Process annotation semantics from legacy implementations (optional, options: mate, molecule)").Default(defaultConfig.Compatibility).EnumVar(&cfg.Compatibility, "", "mate", "molecule")
|
||||
app.Flag("compatibility", "Process annotation semantics from legacy implementations (optional, options: mate, molecule, kops-dns-controller)").Default(defaultConfig.Compatibility).EnumVar(&cfg.Compatibility, "", "mate", "molecule", "kops-dns-controller")
|
||||
app.Flag("ignore-ingress-rules-spec", "Ignore rules spec section in ingresses resources, applicable only for ingress sources (optional, default: false)").BoolVar(&cfg.IgnoreIngressRulesSpec)
|
||||
app.Flag("publish-internal-services", "Allow external-dns to publish DNS records for ClusterIP services (optional)").BoolVar(&cfg.PublishInternal)
|
||||
app.Flag("publish-host-ip", "Allow external-dns to publish host-ip for headless services (optional)").BoolVar(&cfg.PublishHostIP)
|
||||
app.Flag("always-publish-not-ready-addresses", "Always publish also not ready addresses for headless services (optional)").BoolVar(&cfg.AlwaysPublishNotReadyAddresses)
|
||||
@ -338,11 +376,14 @@ func (cfg *Config) ParseFlags(args []string) error {
|
||||
app.Flag("crd-source-kind", "Kind of the CRD for the crd source in API group and version specified by crd-source-apiversion").Default(defaultConfig.CRDSourceKind).StringVar(&cfg.CRDSourceKind)
|
||||
app.Flag("service-type-filter", "The service types to take care about (default: all, expected: ClusterIP, NodePort, LoadBalancer or ExternalName)").StringsVar(&cfg.ServiceTypeFilter)
|
||||
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)
|
||||
app.Flag("default-targets", "Set globally default IP address that will apply as a target instead of source addresses. Specify multiple times for multiple targets (optional)").StringsVar(&cfg.DefaultTargets)
|
||||
|
||||
// Flags related to providers
|
||||
app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, aws-sd, 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")
|
||||
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, gandi)").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", "bluecat", "gandi")
|
||||
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("regex-domain-filter", "Limit possible domains and target zones by a Regex filter; Overrides domain-filter (optional)").Default(defaultConfig.RegexDomainFilter.String()).RegexpVar(&cfg.RegexDomainFilter)
|
||||
app.Flag("regex-domain-exclusion", "Regex filter that excludes domains and target zones matched by regex-domain-filter (optional)").Default(defaultConfig.RegexDomainExclusion.String()).RegexpVar(&cfg.RegexDomainExclusion)
|
||||
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)
|
||||
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)
|
||||
@ -363,6 +404,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)
|
||||
@ -380,6 +422,7 @@ func (cfg *Config) ParseFlags(args []string) error {
|
||||
app.Flag("infoblox-ssl-verify", "When using the Infoblox provider, specify whether to verify the SSL certificate (default: true, disable with --no-infoblox-ssl-verify)").Default(strconv.FormatBool(defaultConfig.InfobloxSSLVerify)).BoolVar(&cfg.InfobloxSSLVerify)
|
||||
app.Flag("infoblox-view", "DNS view (default: \"\")").Default(defaultConfig.InfobloxView).StringVar(&cfg.InfobloxView)
|
||||
app.Flag("infoblox-max-results", "Add _max_results as query parameter to the URL on all API requests. The default is 0 which means _max_results is not set and the default of the server is used.").Default(strconv.Itoa(defaultConfig.InfobloxMaxResults)).IntVar(&cfg.InfobloxMaxResults)
|
||||
app.Flag("infoblox-fqdn-regex", "Apply this regular expression as a filter for obtaining zone_auth objects. This is disabled by default.").Default(defaultConfig.InfobloxFQDNRegEx).StringVar(&cfg.InfobloxFQDNRegEx)
|
||||
app.Flag("dyn-customer-name", "When using the Dyn provider, specify the Customer Name").Default("").StringVar(&cfg.DynCustomerName)
|
||||
app.Flag("dyn-username", "When using the Dyn provider, specify the Username").Default("").StringVar(&cfg.DynUsername)
|
||||
app.Flag("dyn-password", "When using the Dyn provider, specify the password").Default("").StringVar(&cfg.DynPassword)
|
||||
@ -398,6 +441,11 @@ func (cfg *Config) ParseFlags(args []string) error {
|
||||
app.Flag("ns1-ignoressl", "When using the NS1 provider, specify whether to verify the SSL certificate (default: false)").Default(strconv.FormatBool(defaultConfig.NS1IgnoreSSL)).BoolVar(&cfg.NS1IgnoreSSL)
|
||||
app.Flag("ns1-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.NS1MinTTLSeconds)
|
||||
app.Flag("digitalocean-api-page-size", "Configure the page size used when querying the DigitalOcean API.").Default(strconv.Itoa(defaultConfig.DigitalOceanAPIPageSize)).IntVar(&cfg.DigitalOceanAPIPageSize)
|
||||
// GoDaddy flags
|
||||
app.Flag("godaddy-api-key", "When using the GoDaddy provider, specify the API Key (required when --provider=godaddy)").Default(defaultConfig.GoDaddyAPIKey).StringVar(&cfg.GoDaddyAPIKey)
|
||||
app.Flag("godaddy-api-secret", "When using the GoDaddy provider, specify the API secret (required when --provider=godaddy)").Default(defaultConfig.GoDaddySecretKey).StringVar(&cfg.GoDaddySecretKey)
|
||||
app.Flag("godaddy-api-ttl", "TTL (in seconds) for records. This value will be used if the provided TTL for a service/ingress is not provided.").Int64Var(&cfg.GoDaddyTTL)
|
||||
app.Flag("godaddy-api-ote", "When using the GoDaddy provider, use OTE api (optional, default: false, when --provider=godaddy)").BoolVar(&cfg.GoDaddyOTE)
|
||||
|
||||
// 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)
|
||||
@ -418,6 +466,11 @@ func (cfg *Config) ParseFlags(args []string) error {
|
||||
app.Flag("rfc2136-tsig-secret-alg", "When using the RFC2136 provider, specify the TSIG (base64) value to attached to DNS messages (required when --rfc2136-insecure=false)").Default(defaultConfig.RFC2136TSIGSecretAlg).StringVar(&cfg.RFC2136TSIGSecretAlg)
|
||||
app.Flag("rfc2136-tsig-axfr", "When using the RFC2136 provider, specify the TSIG (base64) value to attached to DNS messages (required when --rfc2136-insecure=false)").BoolVar(&cfg.RFC2136TAXFR)
|
||||
app.Flag("rfc2136-min-ttl", "When using the RFC2136 provider, specify minimal TTL (in duration format) for records. This value will be used if the provided TTL for a service/ingress is lower than this").Default(defaultConfig.RFC2136MinTTL.String()).DurationVar(&cfg.RFC2136MinTTL)
|
||||
app.Flag("rfc2136-gss-tsig", "When using the RFC2136 provider, specify whether to use secure updates with GSS-TSIG using Kerberos (default: false, requires --rfc2136-kerberos-realm, --rfc2136-kerberos-username, and rfc2136-kerberos-password)").Default(strconv.FormatBool(defaultConfig.RFC2136GSSTSIG)).BoolVar(&cfg.RFC2136GSSTSIG)
|
||||
app.Flag("rfc2136-kerberos-username", "When using the RFC2136 provider with GSS-TSIG, specify the username of the user with permissions to update DNS records (required when --rfc2136-gss-tsig=true)").Default(defaultConfig.RFC2136KerberosUsername).StringVar(&cfg.RFC2136KerberosUsername)
|
||||
app.Flag("rfc2136-kerberos-password", "When using the RFC2136 provider with GSS-TSIG, specify the password of the user with permissions to update DNS records (required when --rfc2136-gss-tsig=true)").Default(defaultConfig.RFC2136KerberosPassword).StringVar(&cfg.RFC2136KerberosPassword)
|
||||
app.Flag("rfc2136-kerberos-realm", "When using the RFC2136 provider with GSS-TSIG, specify the realm of the user with permissions to update DNS records (required when --rfc2136-gss-tsig=true)").Default(defaultConfig.RFC2136KerberosRealm).StringVar(&cfg.RFC2136KerberosRealm)
|
||||
app.Flag("rfc2136-batch-change-size", "When using the RFC2136 provider, set the maximum number of changes that will be applied in each batch.").Default(strconv.Itoa(defaultConfig.RFC2136BatchChangeSize)).IntVar(&cfg.RFC2136BatchChangeSize)
|
||||
|
||||
// Flags related to TransIP provider
|
||||
app.Flag("transip-account", "When using the TransIP provider, specify the account name (required when --provider=transip)").Default(defaultConfig.TransIPAccountName).StringVar(&cfg.TransIPAccountName)
|
||||
@ -436,6 +489,7 @@ func (cfg *Config) ParseFlags(args []string) error {
|
||||
// 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("min-event-sync-interval", "The minimum interval between two consecutive synchronizations triggered from kubernetes events in duration format (default: 5s)").Default(defaultConfig.MinEventSyncInterval.String()).DurationVar(&cfg.MinEventSyncInterval)
|
||||
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)
|
||||
app.Flag("events", "When enabled, in addition to running every interval, the reconciliation loop will get triggered when supported sources change (default: disabled)").BoolVar(&cfg.UpdateEvents)
|
||||
|
@ -18,6 +18,7 @@ package externaldns
|
||||
|
||||
import (
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@ -35,6 +36,7 @@ var (
|
||||
KubeConfig: "",
|
||||
RequestTimeout: time.Second * 30,
|
||||
ContourLoadBalancerService: "heptio-contour/contour",
|
||||
GlooNamespace: "gloo-system",
|
||||
SkipperRouteGroupVersion: "zalando.org/v1",
|
||||
Sources: []string{"service"},
|
||||
Namespace: "",
|
||||
@ -46,6 +48,8 @@ var (
|
||||
GoogleBatchChangeInterval: time.Second,
|
||||
DomainFilter: []string{""},
|
||||
ExcludeDomains: []string{""},
|
||||
RegexDomainFilter: regexp.MustCompile(""),
|
||||
RegexDomainExclusion: regexp.MustCompile(""),
|
||||
ZoneNameFilter: []string{""},
|
||||
ZoneIDFilter: []string{""},
|
||||
AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json",
|
||||
@ -61,6 +65,7 @@ var (
|
||||
AzureConfigFile: "/etc/kubernetes/azure.json",
|
||||
AzureResourceGroup: "",
|
||||
AzureSubscriptionID: "",
|
||||
BluecatConfigFile: "/etc/kubernetes/bluecat.json",
|
||||
CloudflareProxied: false,
|
||||
CloudflareZonesPerPage: 50,
|
||||
CoreDNSPrefix: "/skydns/",
|
||||
@ -90,6 +95,7 @@ var (
|
||||
TXTPrefix: "",
|
||||
TXTCacheInterval: 0,
|
||||
Interval: time.Minute,
|
||||
MinEventSyncInterval: 5 * time.Second,
|
||||
Once: false,
|
||||
DryRun: false,
|
||||
UpdateEvents: false,
|
||||
@ -107,6 +113,7 @@ var (
|
||||
TransIPPrivateKeyFile: "",
|
||||
DigitalOceanAPIPageSize: 50,
|
||||
ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||
RFC2136BatchChangeSize: 50,
|
||||
}
|
||||
|
||||
overriddenConfig = &Config{
|
||||
@ -114,11 +121,13 @@ var (
|
||||
KubeConfig: "/some/path",
|
||||
RequestTimeout: time.Second * 77,
|
||||
ContourLoadBalancerService: "heptio-contour-other/contour-other",
|
||||
GlooNamespace: "gloo-not-system",
|
||||
SkipperRouteGroupVersion: "zalando.org/v2",
|
||||
Sources: []string{"service", "ingress", "connector"},
|
||||
Namespace: "namespace",
|
||||
IgnoreHostnameAnnotation: true,
|
||||
IgnoreIngressTLSSpec: true,
|
||||
IgnoreIngressRulesSpec: true,
|
||||
FQDNTemplate: "{{.Name}}.service.example.com",
|
||||
Compatibility: "mate",
|
||||
Provider: "google",
|
||||
@ -127,6 +136,8 @@ var (
|
||||
GoogleBatchChangeInterval: time.Second * 2,
|
||||
DomainFilter: []string{"example.org", "company.com"},
|
||||
ExcludeDomains: []string{"xapi.example.org", "xapi.company.com"},
|
||||
RegexDomainFilter: regexp.MustCompile("(example\\.org|company\\.com)$"),
|
||||
RegexDomainExclusion: regexp.MustCompile("xapi\\.(example\\.org|company\\.com)$"),
|
||||
ZoneNameFilter: []string{"yapi.example.org", "yapi.company.com"},
|
||||
ZoneIDFilter: []string{"/hostedzone/ZTST1", "/hostedzone/ZTST2"},
|
||||
AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json",
|
||||
@ -142,6 +153,7 @@ var (
|
||||
AzureConfigFile: "azure.json",
|
||||
AzureResourceGroup: "arg",
|
||||
AzureSubscriptionID: "arg",
|
||||
BluecatConfigFile: "bluecat.json",
|
||||
CloudflareProxied: true,
|
||||
CloudflareZonesPerPage: 20,
|
||||
CoreDNSPrefix: "/coredns/",
|
||||
@ -175,6 +187,7 @@ var (
|
||||
TXTPrefix: "associated-txt-record",
|
||||
TXTCacheInterval: 12 * time.Hour,
|
||||
Interval: 10 * time.Minute,
|
||||
MinEventSyncInterval: 50 * time.Second,
|
||||
Once: true,
|
||||
DryRun: true,
|
||||
UpdateEvents: true,
|
||||
@ -194,6 +207,7 @@ var (
|
||||
TransIPPrivateKeyFile: "/path/to/transip.key",
|
||||
DigitalOceanAPIPageSize: 100,
|
||||
ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||
RFC2136BatchChangeSize: 100,
|
||||
}
|
||||
)
|
||||
|
||||
@ -220,6 +234,7 @@ func TestParseFlags(t *testing.T) {
|
||||
"--kubeconfig=/some/path",
|
||||
"--request-timeout=77s",
|
||||
"--contour-load-balancer=heptio-contour-other/contour-other",
|
||||
"--gloo-namespace=gloo-not-system",
|
||||
"--skipper-routegroup-groupversion=zalando.org/v2",
|
||||
"--source=service",
|
||||
"--source=ingress",
|
||||
@ -228,6 +243,7 @@ func TestParseFlags(t *testing.T) {
|
||||
"--fqdn-template={{.Name}}.service.example.com",
|
||||
"--ignore-hostname-annotation",
|
||||
"--ignore-ingress-tls-spec",
|
||||
"--ignore-ingress-rules-spec",
|
||||
"--compatibility=mate",
|
||||
"--provider=google",
|
||||
"--google-project=project",
|
||||
@ -236,6 +252,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/",
|
||||
@ -268,6 +285,8 @@ func TestParseFlags(t *testing.T) {
|
||||
"--domain-filter=company.com",
|
||||
"--exclude-domains=xapi.example.org",
|
||||
"--exclude-domains=xapi.company.com",
|
||||
"--regex-domain-filter=(example\\.org|company\\.com)$",
|
||||
"--regex-domain-exclusion=xapi\\.(example\\.org|company\\.com)$",
|
||||
"--zone-name-filter=yapi.example.org",
|
||||
"--zone-name-filter=yapi.company.com",
|
||||
"--zone-id-filter=/hostedzone/ZTST1",
|
||||
@ -287,6 +306,7 @@ func TestParseFlags(t *testing.T) {
|
||||
"--txt-prefix=associated-txt-record",
|
||||
"--txt-cache-interval=12h",
|
||||
"--interval=10m",
|
||||
"--min-event-sync-interval=50s",
|
||||
"--once",
|
||||
"--dry-run",
|
||||
"--events",
|
||||
@ -305,6 +325,7 @@ func TestParseFlags(t *testing.T) {
|
||||
"--transip-account=transip",
|
||||
"--transip-keyfile=/path/to/transip.key",
|
||||
"--digitalocean-api-page-size=100",
|
||||
"--rfc2136-batch-change-size=100",
|
||||
},
|
||||
envVars: map[string]string{},
|
||||
expected: overriddenConfig,
|
||||
@ -317,12 +338,14 @@ func TestParseFlags(t *testing.T) {
|
||||
"EXTERNAL_DNS_KUBECONFIG": "/some/path",
|
||||
"EXTERNAL_DNS_REQUEST_TIMEOUT": "77s",
|
||||
"EXTERNAL_DNS_CONTOUR_LOAD_BALANCER": "heptio-contour-other/contour-other",
|
||||
"EXTERNAL_DNS_GLOO_NAMESPACE": "gloo-not-system",
|
||||
"EXTERNAL_DNS_SKIPPER_ROUTEGROUP_GROUPVERSION": "zalando.org/v2",
|
||||
"EXTERNAL_DNS_SOURCE": "service\ningress\nconnector",
|
||||
"EXTERNAL_DNS_NAMESPACE": "namespace",
|
||||
"EXTERNAL_DNS_FQDN_TEMPLATE": "{{.Name}}.service.example.com",
|
||||
"EXTERNAL_DNS_IGNORE_HOSTNAME_ANNOTATION": "1",
|
||||
"EXTERNAL_DNS_IGNORE_INGRESS_TLS_SPEC": "1",
|
||||
"EXTERNAL_DNS_IGNORE_INGRESS_RULES_SPEC": "1",
|
||||
"EXTERNAL_DNS_COMPATIBILITY": "mate",
|
||||
"EXTERNAL_DNS_PROVIDER": "google",
|
||||
"EXTERNAL_DNS_GOOGLE_PROJECT": "project",
|
||||
@ -331,6 +354,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/",
|
||||
@ -354,6 +378,8 @@ func TestParseFlags(t *testing.T) {
|
||||
"EXTERNAL_DNS_OVH_API_RATE_LIMIT": "42",
|
||||
"EXTERNAL_DNS_DOMAIN_FILTER": "example.org\ncompany.com",
|
||||
"EXTERNAL_DNS_EXCLUDE_DOMAINS": "xapi.example.org\nxapi.company.com",
|
||||
"EXTERNAL_DNS_REGEX_DOMAIN_FILTER": "(example\\.org|company\\.com)$",
|
||||
"EXTERNAL_DNS_REGEX_DOMAIN_EXCLUSION": "xapi\\.(example\\.org|company\\.com)$",
|
||||
"EXTERNAL_DNS_PDNS_SERVER": "http://ns.example.com:8081",
|
||||
"EXTERNAL_DNS_PDNS_API_KEY": "some-secret-key",
|
||||
"EXTERNAL_DNS_PDNS_TLS_ENABLED": "1",
|
||||
@ -378,6 +404,7 @@ func TestParseFlags(t *testing.T) {
|
||||
"EXTERNAL_DNS_TXT_PREFIX": "associated-txt-record",
|
||||
"EXTERNAL_DNS_TXT_CACHE_INTERVAL": "12h",
|
||||
"EXTERNAL_DNS_INTERVAL": "10m",
|
||||
"EXTERNAL_DNS_MIN_EVENT_SYNC_INTERVAL": "50s",
|
||||
"EXTERNAL_DNS_ONCE": "1",
|
||||
"EXTERNAL_DNS_DRY_RUN": "1",
|
||||
"EXTERNAL_DNS_EVENTS": "1",
|
||||
@ -396,6 +423,7 @@ func TestParseFlags(t *testing.T) {
|
||||
"EXTERNAL_DNS_TRANSIP_ACCOUNT": "transip",
|
||||
"EXTERNAL_DNS_TRANSIP_KEYFILE": "/path/to/transip.key",
|
||||
"EXTERNAL_DNS_DIGITALOCEAN_API_PAGE_SIZE": "100",
|
||||
"EXTERNAL_DNS_RFC2136_BATCH_CHANGE_SIZE": "100",
|
||||
},
|
||||
expected: overriddenConfig,
|
||||
},
|
||||
|
@ -86,6 +86,20 @@ func ValidateConfig(cfg *externaldns.Config) error {
|
||||
if cfg.RFC2136MinTTL < 0 {
|
||||
return errors.New("TTL specified for rfc2136 is negative")
|
||||
}
|
||||
|
||||
if cfg.RFC2136Insecure && cfg.RFC2136GSSTSIG {
|
||||
return errors.New("--rfc2136-insecure and --rfc2136-gss-tsig are mutually exclusive arguments")
|
||||
}
|
||||
|
||||
if cfg.RFC2136GSSTSIG {
|
||||
if cfg.RFC2136KerberosPassword == "" || cfg.RFC2136KerberosUsername == "" || cfg.RFC2136KerberosRealm == "" {
|
||||
return errors.New("--rfc2136-kerberos-realm, --rfc2136-kerberos-username, and --rfc2136-kerberos-password are required when specifying --rfc2136-gss-tsig option")
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.RFC2136BatchChangeSize < 1 {
|
||||
return errors.New("batch size specified for rfc2136 cannot be less than 1")
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.IgnoreHostnameAnnotation && cfg.FQDNTemplate == "" {
|
||||
|
@ -132,6 +132,21 @@ func TestValidateBadRfc2136Config(t *testing.T) {
|
||||
cfg.Sources = []string{"test-source"}
|
||||
cfg.Provider = "rfc2136"
|
||||
cfg.RFC2136MinTTL = -1
|
||||
cfg.RFC2136BatchChangeSize = 50
|
||||
|
||||
err := ValidateConfig(cfg)
|
||||
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestValidateBadRfc2136Batch(t *testing.T) {
|
||||
cfg := externaldns.NewConfig()
|
||||
|
||||
cfg.LogFormat = "json"
|
||||
cfg.Sources = []string{"test-source"}
|
||||
cfg.Provider = "rfc2136"
|
||||
cfg.RFC2136MinTTL = 3600
|
||||
cfg.RFC2136BatchChangeSize = 0
|
||||
|
||||
err := ValidateConfig(cfg)
|
||||
|
||||
@ -145,8 +160,122 @@ func TestValidateGoodRfc2136Config(t *testing.T) {
|
||||
cfg.Sources = []string{"test-source"}
|
||||
cfg.Provider = "rfc2136"
|
||||
cfg.RFC2136MinTTL = 3600
|
||||
cfg.RFC2136BatchChangeSize = 50
|
||||
|
||||
err := ValidateConfig(cfg)
|
||||
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestValidateBadRfc2136GssTsigConfig(t *testing.T) {
|
||||
var invalidRfc2136GssTsigConfigs = []*externaldns.Config{
|
||||
{
|
||||
LogFormat: "json",
|
||||
Sources: []string{"test-source"},
|
||||
Provider: "rfc2136",
|
||||
RFC2136GSSTSIG: true,
|
||||
RFC2136KerberosRealm: "test-realm",
|
||||
RFC2136KerberosUsername: "test-user",
|
||||
RFC2136KerberosPassword: "",
|
||||
RFC2136MinTTL: 3600,
|
||||
RFC2136BatchChangeSize: 50,
|
||||
},
|
||||
{
|
||||
LogFormat: "json",
|
||||
Sources: []string{"test-source"},
|
||||
Provider: "rfc2136",
|
||||
RFC2136GSSTSIG: true,
|
||||
RFC2136KerberosRealm: "test-realm",
|
||||
RFC2136KerberosUsername: "",
|
||||
RFC2136KerberosPassword: "test-pass",
|
||||
RFC2136MinTTL: 3600,
|
||||
RFC2136BatchChangeSize: 50,
|
||||
},
|
||||
{
|
||||
LogFormat: "json",
|
||||
Sources: []string{"test-source"},
|
||||
Provider: "rfc2136",
|
||||
RFC2136GSSTSIG: true,
|
||||
RFC2136Insecure: true,
|
||||
RFC2136KerberosRealm: "test-realm",
|
||||
RFC2136KerberosUsername: "test-user",
|
||||
RFC2136KerberosPassword: "test-pass",
|
||||
RFC2136MinTTL: 3600,
|
||||
RFC2136BatchChangeSize: 50,
|
||||
},
|
||||
{
|
||||
LogFormat: "json",
|
||||
Sources: []string{"test-source"},
|
||||
Provider: "rfc2136",
|
||||
RFC2136GSSTSIG: true,
|
||||
RFC2136KerberosRealm: "",
|
||||
RFC2136KerberosUsername: "test-user",
|
||||
RFC2136KerberosPassword: "",
|
||||
RFC2136MinTTL: 3600,
|
||||
RFC2136BatchChangeSize: 50,
|
||||
},
|
||||
{
|
||||
LogFormat: "json",
|
||||
Sources: []string{"test-source"},
|
||||
Provider: "rfc2136",
|
||||
RFC2136GSSTSIG: true,
|
||||
RFC2136KerberosRealm: "",
|
||||
RFC2136KerberosUsername: "",
|
||||
RFC2136KerberosPassword: "test-pass",
|
||||
RFC2136MinTTL: 3600,
|
||||
RFC2136BatchChangeSize: 50,
|
||||
},
|
||||
{
|
||||
LogFormat: "json",
|
||||
Sources: []string{"test-source"},
|
||||
Provider: "rfc2136",
|
||||
RFC2136GSSTSIG: true,
|
||||
RFC2136Insecure: true,
|
||||
RFC2136KerberosRealm: "",
|
||||
RFC2136KerberosUsername: "test-user",
|
||||
RFC2136KerberosPassword: "test-pass",
|
||||
RFC2136MinTTL: 3600,
|
||||
RFC2136BatchChangeSize: 50,
|
||||
},
|
||||
{
|
||||
LogFormat: "json",
|
||||
Sources: []string{"test-source"},
|
||||
Provider: "rfc2136",
|
||||
RFC2136GSSTSIG: true,
|
||||
RFC2136KerberosRealm: "",
|
||||
RFC2136KerberosUsername: "test-user",
|
||||
RFC2136KerberosPassword: "test-pass",
|
||||
RFC2136MinTTL: 3600,
|
||||
RFC2136BatchChangeSize: 50,
|
||||
},
|
||||
}
|
||||
|
||||
for _, cfg := range invalidRfc2136GssTsigConfigs {
|
||||
err := ValidateConfig(cfg)
|
||||
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateGoodRfc2136GssTsigConfig(t *testing.T) {
|
||||
var validRfc2136GssTsigConfigs = []*externaldns.Config{
|
||||
{
|
||||
LogFormat: "json",
|
||||
Sources: []string{"test-source"},
|
||||
Provider: "rfc2136",
|
||||
RFC2136GSSTSIG: true,
|
||||
RFC2136Insecure: false,
|
||||
RFC2136KerberosRealm: "test-realm",
|
||||
RFC2136KerberosUsername: "test-user",
|
||||
RFC2136KerberosPassword: "test-pass",
|
||||
RFC2136MinTTL: 3600,
|
||||
RFC2136BatchChangeSize: 50,
|
||||
},
|
||||
}
|
||||
|
||||
for _, cfg := range validRfc2136GssTsigConfigs {
|
||||
err := ValidateConfig(cfg)
|
||||
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
}
|
||||
|
19
plan/plan.go
19
plan/plan.go
@ -21,6 +21,9 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
)
|
||||
|
||||
@ -40,7 +43,7 @@ type Plan struct {
|
||||
// Populated after calling Calculate()
|
||||
Changes *Changes
|
||||
// DomainFilter matches DNS names
|
||||
DomainFilter endpoint.DomainFilter
|
||||
DomainFilter endpoint.DomainFilterInterface
|
||||
// Property comparator compares custom properties of providers
|
||||
PropertyComparator PropertyComparator
|
||||
// DNS record types that will be considered for management
|
||||
@ -115,12 +118,23 @@ func (t planTable) addCandidate(e *endpoint.Endpoint) {
|
||||
t.rows[dnsName][e.SetIdentifier].candidates = append(t.rows[dnsName][e.SetIdentifier].candidates, e)
|
||||
}
|
||||
|
||||
func (c *Changes) HasChanges() bool {
|
||||
if len(c.Create) > 0 || len(c.Delete) > 0 {
|
||||
return true
|
||||
}
|
||||
return !cmp.Equal(c.UpdateNew, c.UpdateOld)
|
||||
}
|
||||
|
||||
// Calculate computes the actions needed to move current state towards desired
|
||||
// state. It then passes those changes to the current policy for further
|
||||
// processing. It returns a copy of Plan with the changes populated.
|
||||
func (p *Plan) Calculate() *Plan {
|
||||
t := newPlanTable()
|
||||
|
||||
if p.DomainFilter == nil {
|
||||
p.DomainFilter = endpoint.MatchAllDomainFilters(nil)
|
||||
}
|
||||
|
||||
for _, current := range filterRecordsForPlan(p.Current, p.DomainFilter, p.ManagedRecords) {
|
||||
t.addCurrent(current)
|
||||
}
|
||||
@ -227,12 +241,13 @@ func (p *Plan) shouldUpdateProviderSpecific(desired, current *endpoint.Endpoint)
|
||||
// Per RFC 1034, CNAME records conflict with all other records - it is the
|
||||
// only record with this property. The behavior of the planner may need to be
|
||||
// made more sophisticated to codify this.
|
||||
func filterRecordsForPlan(records []*endpoint.Endpoint, domainFilter endpoint.DomainFilter, managedRecords []string) []*endpoint.Endpoint {
|
||||
func filterRecordsForPlan(records []*endpoint.Endpoint, domainFilter endpoint.DomainFilterInterface, managedRecords []string) []*endpoint.Endpoint {
|
||||
filtered := []*endpoint.Endpoint{}
|
||||
|
||||
for _, record := range records {
|
||||
// Ignore records that do not match the domain filter provided
|
||||
if !domainFilter.Match(record.DNSName) {
|
||||
log.Debugf("ignoring record %s that does not match domain filter", record.DNSName)
|
||||
continue
|
||||
}
|
||||
if isManagedRecord(record.RecordType, managedRecords) {
|
||||
|
@ -30,6 +30,7 @@ type PlanTestSuite struct {
|
||||
suite.Suite
|
||||
fooV1Cname *endpoint.Endpoint
|
||||
fooV2Cname *endpoint.Endpoint
|
||||
fooV2CnameUppercase *endpoint.Endpoint
|
||||
fooV2TXT *endpoint.Endpoint
|
||||
fooV2CnameNoLabel *endpoint.Endpoint
|
||||
fooV3CnameSameResource *endpoint.Endpoint
|
||||
@ -77,6 +78,14 @@ func (suite *PlanTestSuite) SetupTest() {
|
||||
endpoint.ResourceLabelKey: "ingress/default/foo-v2",
|
||||
},
|
||||
}
|
||||
suite.fooV2CnameUppercase = &endpoint.Endpoint{
|
||||
DNSName: "foo",
|
||||
Targets: endpoint.Targets{"V2"},
|
||||
RecordType: "CNAME",
|
||||
Labels: map[string]string{
|
||||
endpoint.ResourceLabelKey: "ingress/default/foo-v2",
|
||||
},
|
||||
}
|
||||
suite.fooV2TXT = &endpoint.Endpoint{
|
||||
DNSName: "foo",
|
||||
RecordType: "TXT",
|
||||
@ -452,6 +461,27 @@ func (suite *PlanTestSuite) TestIgnoreTXT() {
|
||||
validateEntries(suite.T(), changes.Delete, expectedDelete)
|
||||
}
|
||||
|
||||
func (suite *PlanTestSuite) TestIgnoreTargetCase() {
|
||||
current := []*endpoint.Endpoint{suite.fooV2Cname}
|
||||
desired := []*endpoint.Endpoint{suite.fooV2CnameUppercase}
|
||||
expectedCreate := []*endpoint.Endpoint{}
|
||||
expectedUpdateOld := []*endpoint.Endpoint{}
|
||||
expectedUpdateNew := []*endpoint.Endpoint{}
|
||||
expectedDelete := []*endpoint.Endpoint{}
|
||||
|
||||
p := &Plan{
|
||||
Policies: []Policy{&SyncPolicy{}},
|
||||
Current: current,
|
||||
Desired: desired,
|
||||
}
|
||||
|
||||
changes := p.Calculate().Changes
|
||||
validateEntries(suite.T(), changes.Create, expectedCreate)
|
||||
validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew)
|
||||
validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld)
|
||||
validateEntries(suite.T(), changes.Delete, expectedDelete)
|
||||
}
|
||||
|
||||
func (suite *PlanTestSuite) TestRemoveEndpoint() {
|
||||
current := []*endpoint.Endpoint{suite.fooV1Cname, suite.bar192A}
|
||||
desired := []*endpoint.Endpoint{suite.fooV1Cname}
|
||||
|
@ -40,8 +40,17 @@ import (
|
||||
|
||||
const (
|
||||
recordTTL = 300
|
||||
// From the experiments, it seems that the default MaxItems applied is 100,
|
||||
// and that, on the server side, there is a hard limit of 300 elements per page.
|
||||
// After a discussion with AWS representants, clients should accept
|
||||
// when less items are returned, and still paginate accordingly.
|
||||
// As we are using the standard AWS client, this should already be compliant.
|
||||
// Hence, ifever AWS decides to raise this limit, we will automatically reduce the pressure on rate limits
|
||||
route53PageSize = "300"
|
||||
// provider specific key that designates whether an AWS ALIAS record has the EvaluateTargetHealth
|
||||
// field set to true.
|
||||
providerSpecificAlias = "alias"
|
||||
providerSpecificTargetHostedZone = "aws/target-hosted-zone"
|
||||
providerSpecificEvaluateTargetHealth = "aws/evaluate-target-health"
|
||||
providerSpecificWeight = "aws/weight"
|
||||
providerSpecificRegion = "aws/region"
|
||||
@ -51,6 +60,7 @@ const (
|
||||
providerSpecificGeolocationSubdivisionCode = "aws/geolocation-subdivision-code"
|
||||
providerSpecificMultiValueAnswer = "aws/multi-value-answer"
|
||||
providerSpecificHealthCheckID = "aws/health-check-id"
|
||||
sameZoneAlias = "same-zone"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -74,6 +84,7 @@ var (
|
||||
"eu-west-2.elb.amazonaws.com": "ZHURV8PSTC4K8",
|
||||
"eu-west-3.elb.amazonaws.com": "Z3Q77PNBQS71R4",
|
||||
"eu-north-1.elb.amazonaws.com": "Z23TAZ6LKFMNIO",
|
||||
"eu-south-1.elb.amazonaws.com": "Z3ULH7SSC9OV64",
|
||||
"sa-east-1.elb.amazonaws.com": "Z2P70J7HTTTPLU",
|
||||
"cn-north-1.elb.amazonaws.com.cn": "Z1GDH35T77C1KE",
|
||||
"cn-northwest-1.elb.amazonaws.com.cn": "ZM7IZAIOVVDZF",
|
||||
@ -98,6 +109,7 @@ var (
|
||||
"elb.eu-west-2.amazonaws.com": "ZD4D7Y8KGAS4G",
|
||||
"elb.eu-west-3.amazonaws.com": "Z1CMS0P5QUZ6D5",
|
||||
"elb.eu-north-1.amazonaws.com": "Z1UDT6IFJ4EJM",
|
||||
"elb.eu-south-1.amazonaws.com": "Z23146JA1KNAFP",
|
||||
"elb.sa-east-1.amazonaws.com": "ZTK26PT1VY4CU",
|
||||
"elb.cn-north-1.amazonaws.com.cn": "Z3QFB96KMJ7ED6",
|
||||
"elb.cn-northwest-1.amazonaws.com.cn": "ZQEIKTCZ8352D",
|
||||
@ -328,7 +340,8 @@ func (p *AWSProvider) records(ctx context.Context, zones map[string]*route53.Hos
|
||||
}
|
||||
ep := endpoint.
|
||||
NewEndpointWithTTL(wildcardUnescape(aws.StringValue(r.Name)), endpoint.RecordTypeCNAME, ttl, aws.StringValue(r.AliasTarget.DNSName)).
|
||||
WithProviderSpecific(providerSpecificEvaluateTargetHealth, fmt.Sprintf("%t", aws.BoolValue(r.AliasTarget.EvaluateTargetHealth)))
|
||||
WithProviderSpecific(providerSpecificEvaluateTargetHealth, fmt.Sprintf("%t", aws.BoolValue(r.AliasTarget.EvaluateTargetHealth))).
|
||||
WithProviderSpecific(providerSpecificAlias, "true")
|
||||
newEndpoints = append(newEndpoints, ep)
|
||||
}
|
||||
|
||||
@ -374,6 +387,7 @@ func (p *AWSProvider) records(ctx context.Context, zones map[string]*route53.Hos
|
||||
for _, z := range zones {
|
||||
params := &route53.ListResourceRecordSetsInput{
|
||||
HostedZoneId: z.Id,
|
||||
MaxItems: aws.String(route53PageSize),
|
||||
}
|
||||
|
||||
if err := p.client.ListResourceRecordSetsPagesWithContext(ctx, params, f); err != nil {
|
||||
@ -404,6 +418,9 @@ func (p *AWSProvider) doRecords(ctx context.Context, action string, endpoints []
|
||||
if err != nil {
|
||||
log.Errorf("failed to list records while preparing %s doRecords action: %s", action, err)
|
||||
}
|
||||
|
||||
p.AdjustEndpoints(endpoints)
|
||||
|
||||
return p.submitChanges(ctx, p.newChanges(action, endpoints, records, zones), zones)
|
||||
}
|
||||
|
||||
@ -448,6 +465,21 @@ func (p *AWSProvider) createUpdateChanges(newEndpoints, oldEndpoints []*endpoint
|
||||
return combined
|
||||
}
|
||||
|
||||
// GetDomainFilter generates a filter to exclude any domain that is not controlled by the provider
|
||||
func (p *AWSProvider) GetDomainFilter() endpoint.DomainFilterInterface {
|
||||
zones, err := p.Zones(context.Background())
|
||||
if err != nil {
|
||||
log.Errorf("failed to list zones: %v", err)
|
||||
return &endpoint.DomainFilter{}
|
||||
}
|
||||
zoneNames := []string(nil)
|
||||
for _, z := range zones {
|
||||
zoneNames = append(zoneNames, aws.StringValue(z.Name), "."+aws.StringValue(z.Name))
|
||||
}
|
||||
log.Infof("Applying provider record filter for domains: %v", zoneNames)
|
||||
return endpoint.NewDomainFilter(zoneNames)
|
||||
}
|
||||
|
||||
// ApplyChanges applies a given set of changes in a given zone.
|
||||
func (p *AWSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
|
||||
zones, err := p.Zones(ctx)
|
||||
@ -553,6 +585,36 @@ func (p *AWSProvider) newChanges(action string, endpoints []*endpoint.Endpoint,
|
||||
return changes
|
||||
}
|
||||
|
||||
// AdjustEndpoints modifies the provided endpoints (coming from various sources) to match
|
||||
// the endpoints that the provider returns in `Records` so that the change plan will not have
|
||||
// unneeded (potentially failing) changes.
|
||||
// Example: CNAME endpoints pointing to ELBs will have a `alias` provider-specific property
|
||||
// added to match the endpoints generated from existing alias records in Route53.
|
||||
func (p *AWSProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) []*endpoint.Endpoint {
|
||||
for _, ep := range endpoints {
|
||||
alias := false
|
||||
if aliasString, ok := ep.GetProviderSpecificProperty(providerSpecificAlias); ok {
|
||||
alias = aliasString.Value == "true"
|
||||
} else if useAlias(ep, p.preferCNAME) {
|
||||
alias = true
|
||||
log.Debugf("Modifying endpoint: %v, setting %s=true", ep, providerSpecificAlias)
|
||||
ep.ProviderSpecific = append(ep.ProviderSpecific, endpoint.ProviderSpecificProperty{
|
||||
Name: providerSpecificAlias,
|
||||
Value: "true",
|
||||
})
|
||||
}
|
||||
|
||||
if _, ok := ep.GetProviderSpecificProperty(providerSpecificEvaluateTargetHealth); alias && !ok {
|
||||
log.Debugf("Modifying endpoint: %v, setting %s=%t", ep, providerSpecificEvaluateTargetHealth, p.evaluateTargetHealth)
|
||||
ep.ProviderSpecific = append(ep.ProviderSpecific, endpoint.ProviderSpecificProperty{
|
||||
Name: providerSpecificEvaluateTargetHealth,
|
||||
Value: fmt.Sprintf("%t", p.evaluateTargetHealth),
|
||||
})
|
||||
}
|
||||
}
|
||||
return endpoints
|
||||
}
|
||||
|
||||
// newChange returns a route53 Change and a boolean indicating if there should also be a change to a AAAA record
|
||||
// returned Change is based on the given record by the given action, e.g.
|
||||
// action=ChangeActionCreate returns a change for creation of the record and
|
||||
@ -565,8 +627,7 @@ func (p *AWSProvider) newChange(action string, ep *endpoint.Endpoint, recordsCac
|
||||
},
|
||||
}
|
||||
dualstack := false
|
||||
|
||||
if useAlias(ep, p.preferCNAME) {
|
||||
if targetHostedZone := isAWSAlias(ep); targetHostedZone != "" {
|
||||
evalTargetHealth := p.evaluateTargetHealth
|
||||
if prop, ok := ep.GetProviderSpecificProperty(providerSpecificEvaluateTargetHealth); ok {
|
||||
evalTargetHealth = prop.Value == "true"
|
||||
@ -575,22 +636,12 @@ func (p *AWSProvider) newChange(action string, ep *endpoint.Endpoint, recordsCac
|
||||
if val, ok := ep.Labels[endpoint.DualstackLabelKey]; ok {
|
||||
dualstack = val == "true"
|
||||
}
|
||||
|
||||
change.ResourceRecordSet.Type = aws.String(route53.RRTypeA)
|
||||
change.ResourceRecordSet.AliasTarget = &route53.AliasTarget{
|
||||
DNSName: aws.String(ep.Targets[0]),
|
||||
HostedZoneId: aws.String(canonicalHostedZone(ep.Targets[0])),
|
||||
HostedZoneId: aws.String(cleanZoneID(targetHostedZone)),
|
||||
EvaluateTargetHealth: aws.Bool(evalTargetHealth),
|
||||
}
|
||||
} else if hostedZone := isAWSAlias(ep, recordsCache); hostedZone != "" {
|
||||
for _, zone := range zones {
|
||||
change.ResourceRecordSet.Type = aws.String(route53.RRTypeA)
|
||||
change.ResourceRecordSet.AliasTarget = &route53.AliasTarget{
|
||||
DNSName: aws.String(ep.Targets[0]),
|
||||
HostedZoneId: aws.String(cleanZoneID(*zone.Id)),
|
||||
EvaluateTargetHealth: aws.Bool(p.evaluateTargetHealth),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
change.ResourceRecordSet.Type = aws.String(ep.RecordType)
|
||||
if !ep.RecordTTL.IsConfigured() {
|
||||
@ -754,6 +805,18 @@ func changesByZone(zones map[string]*route53.HostedZone, changeSet []*route53.Ch
|
||||
continue
|
||||
}
|
||||
for _, z := range zones {
|
||||
if c.ResourceRecordSet.AliasTarget != nil && aws.StringValue(c.ResourceRecordSet.AliasTarget.HostedZoneId) == sameZoneAlias {
|
||||
// alias record is to be created; target needs to be in the same zone as endpoint
|
||||
// if it's not, this will fail
|
||||
rrset := *c.ResourceRecordSet
|
||||
aliasTarget := *rrset.AliasTarget
|
||||
aliasTarget.HostedZoneId = aws.String(cleanZoneID(aws.StringValue(z.Id)))
|
||||
rrset.AliasTarget = &aliasTarget
|
||||
c = &route53.Change{
|
||||
Action: c.Action,
|
||||
ResourceRecordSet: &rrset,
|
||||
}
|
||||
}
|
||||
changes[aws.StringValue(z.Id)] = append(changes[aws.StringValue(z.Id)], c)
|
||||
log.Debugf("Adding %s to zone %s [Id: %s]", hostname, aws.StringValue(z.Name), aws.StringValue(z.Id))
|
||||
}
|
||||
@ -809,16 +872,25 @@ func useAlias(ep *endpoint.Endpoint, preferCNAME bool) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// isAWSAlias determines if a given hostname belongs to an AWS Alias record by doing an reverse lookup.
|
||||
func isAWSAlias(ep *endpoint.Endpoint, addrs []*endpoint.Endpoint) string {
|
||||
if prop, exists := ep.GetProviderSpecificProperty("alias"); ep.RecordType == endpoint.RecordTypeCNAME && exists && prop.Value == "true" {
|
||||
for _, addr := range addrs {
|
||||
if len(ep.Targets) > 0 && addr.DNSName == ep.Targets[0] {
|
||||
if hostedZone := canonicalHostedZone(addr.Targets[0]); hostedZone != "" {
|
||||
return hostedZone
|
||||
}
|
||||
// isAWSAlias determines if a given endpoint is supposed to create an AWS Alias record
|
||||
// and (if so) returns the target hosted zone ID
|
||||
func isAWSAlias(ep *endpoint.Endpoint) string {
|
||||
prop, exists := ep.GetProviderSpecificProperty(providerSpecificAlias)
|
||||
if exists && prop.Value == "true" && ep.RecordType == endpoint.RecordTypeCNAME && len(ep.Targets) > 0 {
|
||||
// alias records can only point to canonical hosted zones (e.g. to ELBs) or other records in the same zone
|
||||
|
||||
if hostedZoneID, ok := ep.GetProviderSpecificProperty(providerSpecificTargetHostedZone); ok {
|
||||
// existing Endpoint where we got the target hosted zone from the Route53 data
|
||||
return hostedZoneID.Value
|
||||
}
|
||||
|
||||
// check if the target is in a canonical hosted zone
|
||||
if canonicalHostedZone := canonicalHostedZone(ep.Targets[0]); canonicalHostedZone != "" {
|
||||
return canonicalHostedZone
|
||||
}
|
||||
|
||||
// if not, target needs to be in the same zone
|
||||
return sameZoneAlias
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
@ -56,6 +56,7 @@ type Route53APIStub struct {
|
||||
recordSets map[string]map[string][]*route53.ResourceRecordSet
|
||||
zoneTags map[string][]*route53.Tag
|
||||
m dynamicMock
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
// MockMethod starts a description of an expectation of the specified method
|
||||
@ -67,16 +68,19 @@ func (r *Route53APIStub) MockMethod(method string, args ...interface{}) *mock.Ca
|
||||
}
|
||||
|
||||
// NewRoute53APIStub returns an initialized Route53APIStub
|
||||
func NewRoute53APIStub() *Route53APIStub {
|
||||
func NewRoute53APIStub(t *testing.T) *Route53APIStub {
|
||||
return &Route53APIStub{
|
||||
zones: make(map[string]*route53.HostedZone),
|
||||
recordSets: make(map[string]map[string][]*route53.ResourceRecordSet),
|
||||
zoneTags: make(map[string][]*route53.Tag),
|
||||
t: t,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Route53APIStub) ListResourceRecordSetsPagesWithContext(ctx context.Context, input *route53.ListResourceRecordSetsInput, fn func(p *route53.ListResourceRecordSetsOutput, lastPage bool) (shouldContinue bool), opts ...request.Option) error {
|
||||
output := route53.ListResourceRecordSetsOutput{} // TODO: Support optional input args.
|
||||
require.NotNil(r.t, input.MaxItems)
|
||||
assert.EqualValues(r.t, route53PageSize, *input.MaxItems)
|
||||
if len(r.recordSets) == 0 {
|
||||
output.ResourceRecordSets = []*route53.ResourceRecordSet{}
|
||||
} else if _, ok := r.recordSets[aws.StringValue(input.HostedZoneId)]; !ok {
|
||||
@ -309,6 +313,29 @@ func TestAWSZones(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAWSRecordsFilter(t *testing.T) {
|
||||
provider, _ := newAWSProvider(t, endpoint.DomainFilter{}, provider.ZoneIDFilter{}, provider.ZoneTypeFilter{}, false, false, nil)
|
||||
domainFilter := provider.GetDomainFilter()
|
||||
assert.NotNil(t, domainFilter)
|
||||
require.IsType(t, endpoint.DomainFilter{}, domainFilter)
|
||||
count := 0
|
||||
filters := domainFilter.(endpoint.DomainFilter).Filters
|
||||
for _, tld := range []string{
|
||||
"zone-4.ext-dns-test-3.teapot.zalan.do",
|
||||
".zone-4.ext-dns-test-3.teapot.zalan.do",
|
||||
"zone-2.ext-dns-test-2.teapot.zalan.do",
|
||||
".zone-2.ext-dns-test-2.teapot.zalan.do",
|
||||
"zone-3.ext-dns-test-2.teapot.zalan.do",
|
||||
".zone-3.ext-dns-test-2.teapot.zalan.do",
|
||||
"zone-4.ext-dns-test-3.teapot.zalan.do",
|
||||
".zone-4.ext-dns-test-3.teapot.zalan.do",
|
||||
} {
|
||||
assert.Contains(t, filters, tld)
|
||||
count++
|
||||
}
|
||||
assert.Len(t, filters, count)
|
||||
}
|
||||
|
||||
func TestAWSRecords(t *testing.T) {
|
||||
provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), false, 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"),
|
||||
@ -337,9 +364,9 @@ func TestAWSRecords(t *testing.T) {
|
||||
endpoint.NewEndpointWithTTL("list-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4"),
|
||||
endpoint.NewEndpointWithTTL("list-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"),
|
||||
endpoint.NewEndpointWithTTL("*.wildcard-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"),
|
||||
endpoint.NewEndpointWithTTL("list-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false"),
|
||||
endpoint.NewEndpointWithTTL("*.wildcard-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false"),
|
||||
endpoint.NewEndpointWithTTL("list-test-alias-evaluate.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true"),
|
||||
endpoint.NewEndpointWithTTL("list-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false").WithProviderSpecific(providerSpecificAlias, "true"),
|
||||
endpoint.NewEndpointWithTTL("*.wildcard-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false").WithProviderSpecific(providerSpecificAlias, "true"),
|
||||
endpoint.NewEndpointWithTTL("list-test-alias-evaluate.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true").WithProviderSpecific(providerSpecificAlias, "true"),
|
||||
endpoint.NewEndpointWithTTL("list-test-multiple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8", "8.8.4.4"),
|
||||
endpoint.NewEndpointWithTTL("prefix-*.wildcard.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeTXT, endpoint.TTL(recordTTL), "random"),
|
||||
endpoint.NewEndpointWithTTL("weight-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificWeight, "10"),
|
||||
@ -354,6 +381,30 @@ func TestAWSRecords(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestAWSAdjustEndpoints(t *testing.T) {
|
||||
provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{})
|
||||
|
||||
records := []*endpoint.Endpoint{
|
||||
endpoint.NewEndpoint("a-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
|
||||
endpoint.NewEndpoint("cname-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.example.com"),
|
||||
endpoint.NewEndpoint("cname-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "alias-target.zone-2.ext-dns-test-2.teapot.zalan.do").WithProviderSpecific(providerSpecificAlias, "true"),
|
||||
endpoint.NewEndpoint("cname-test-elb.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com"),
|
||||
endpoint.NewEndpoint("cname-test-elb-no-alias.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "false"),
|
||||
endpoint.NewEndpoint("cname-test-elb-no-eth.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false"), // eth = evaluate target health
|
||||
}
|
||||
|
||||
provider.AdjustEndpoints(records)
|
||||
|
||||
validateEndpoints(t, records, []*endpoint.Endpoint{
|
||||
endpoint.NewEndpoint("a-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
|
||||
endpoint.NewEndpoint("cname-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.example.com"),
|
||||
endpoint.NewEndpoint("cname-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "alias-target.zone-2.ext-dns-test-2.teapot.zalan.do").WithProviderSpecific(providerSpecificAlias, "true").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true"),
|
||||
endpoint.NewEndpoint("cname-test-elb.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true"),
|
||||
endpoint.NewEndpoint("cname-test-elb-no-alias.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "false"),
|
||||
endpoint.NewEndpoint("cname-test-elb-no-eth.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false"), // eth = evaluate target health
|
||||
})
|
||||
}
|
||||
|
||||
func TestAWSCreateRecords(t *testing.T) {
|
||||
customTTL := endpoint.TTL(60)
|
||||
provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{})
|
||||
@ -361,8 +412,10 @@ func TestAWSCreateRecords(t *testing.T) {
|
||||
records := []*endpoint.Endpoint{
|
||||
endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4"),
|
||||
endpoint.NewEndpoint("create-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
|
||||
endpoint.NewEndpointWithTTL("create-test-cname-custom-ttl.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, customTTL, "172.17.0.1"),
|
||||
endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"),
|
||||
endpoint.NewEndpointWithTTL("create-test-custom-ttl.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, customTTL, "172.17.0.1"),
|
||||
endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.example.com"),
|
||||
endpoint.NewEndpoint("create-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com"),
|
||||
endpoint.NewEndpoint("create-test-cname-alias.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "alias-target.zone-2.ext-dns-test-2.teapot.zalan.do").WithProviderSpecific(providerSpecificAlias, "true"),
|
||||
endpoint.NewEndpoint("create-test-multiple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8", "8.8.4.4"),
|
||||
}
|
||||
|
||||
@ -374,8 +427,10 @@ func TestAWSCreateRecords(t *testing.T) {
|
||||
validateEndpoints(t, records, []*endpoint.Endpoint{
|
||||
endpoint.NewEndpointWithTTL("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4"),
|
||||
endpoint.NewEndpointWithTTL("create-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"),
|
||||
endpoint.NewEndpointWithTTL("create-test-cname-custom-ttl.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, customTTL, "172.17.0.1"),
|
||||
endpoint.NewEndpointWithTTL("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.elb.amazonaws.com"),
|
||||
endpoint.NewEndpointWithTTL("create-test-custom-ttl.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, customTTL, "172.17.0.1"),
|
||||
endpoint.NewEndpointWithTTL("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.example.com"),
|
||||
endpoint.NewEndpointWithTTL("create-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true").WithProviderSpecific(providerSpecificAlias, "true"),
|
||||
endpoint.NewEndpointWithTTL("create-test-cname-alias.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "alias-target.zone-2.ext-dns-test-2.teapot.zalan.do").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true").WithProviderSpecific(providerSpecificAlias, "true"),
|
||||
endpoint.NewEndpointWithTTL("create-test-multiple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8", "8.8.4.4"),
|
||||
})
|
||||
}
|
||||
@ -425,6 +480,7 @@ func TestAWSDeleteRecords(t *testing.T) {
|
||||
endpoint.NewEndpointWithTTL("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "baz.elb.amazonaws.com"),
|
||||
endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false"),
|
||||
endpoint.NewEndpoint("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true"),
|
||||
endpoint.NewEndpoint("delete-test-cname-alias.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "delete-test.zone-2.ext-dns-test-2.teapot.zalan.do").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true").WithProviderSpecific(providerSpecificAlias, "true").WithProviderSpecific(providerSpecificTargetHostedZone, "/hostedzone/zone-2.ext-dns-test-2.teapot.zalan.do."),
|
||||
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"),
|
||||
}
|
||||
|
||||
@ -461,7 +517,7 @@ func TestAWSApplyChanges(t *testing.T) {
|
||||
endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.4.4"),
|
||||
endpoint.NewEndpointWithTTL("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.4.4"),
|
||||
endpoint.NewEndpointWithTTL("update-test-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.1.1.1"),
|
||||
endpoint.NewEndpointWithTTL("update-test-alias-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.eu-central-1.elb.amazonaws.com"),
|
||||
endpoint.NewEndpointWithTTL("update-test-alias-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true"),
|
||||
endpoint.NewEndpointWithTTL("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "bar.elb.amazonaws.com"),
|
||||
endpoint.NewEndpointWithTTL("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "qux.elb.amazonaws.com"),
|
||||
endpoint.NewEndpointWithTTL("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "bar.elb.amazonaws.com"),
|
||||
@ -482,7 +538,7 @@ func TestAWSApplyChanges(t *testing.T) {
|
||||
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
|
||||
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.4.4"),
|
||||
endpoint.NewEndpoint("update-test-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.1.1.1"),
|
||||
endpoint.NewEndpoint("update-test-alias-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com"),
|
||||
endpoint.NewEndpoint("update-test-alias-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true"),
|
||||
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "bar.elb.amazonaws.com"),
|
||||
endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "bar.elb.amazonaws.com"),
|
||||
endpoint.NewEndpoint("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8", "8.8.4.4"),
|
||||
@ -490,7 +546,7 @@ func TestAWSApplyChanges(t *testing.T) {
|
||||
updatedRecords := []*endpoint.Endpoint{
|
||||
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4"),
|
||||
endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "4.3.2.1"),
|
||||
endpoint.NewEndpoint("update-test-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"),
|
||||
endpoint.NewEndpoint("update-test-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true"),
|
||||
endpoint.NewEndpoint("update-test-alias-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "my-internal-host.example.com"),
|
||||
endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "baz.elb.amazonaws.com"),
|
||||
endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "baz.elb.amazonaws.com"),
|
||||
@ -530,7 +586,7 @@ func TestAWSApplyChanges(t *testing.T) {
|
||||
endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4"),
|
||||
endpoint.NewEndpointWithTTL("create-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.4.4"),
|
||||
endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "4.3.2.1"),
|
||||
endpoint.NewEndpointWithTTL("update-test-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.elb.amazonaws.com"),
|
||||
endpoint.NewEndpointWithTTL("update-test-a-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true").WithProviderSpecific(providerSpecificAlias, "true"),
|
||||
endpoint.NewEndpointWithTTL("update-test-alias-to-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "my-internal-host.example.com"),
|
||||
endpoint.NewEndpointWithTTL("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.elb.amazonaws.com"),
|
||||
endpoint.NewEndpointWithTTL("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "baz.elb.amazonaws.com"),
|
||||
@ -841,7 +897,7 @@ func TestAWSBatchChangeSetExceedingNameChange(t *testing.T) {
|
||||
}
|
||||
|
||||
func validateEndpoints(t *testing.T, endpoints []*endpoint.Endpoint, expected []*endpoint.Endpoint) {
|
||||
assert.True(t, testutils.SameEndpoints(endpoints, expected), "actual and expected endpoints don't match. %s:%s", endpoints, expected)
|
||||
assert.True(t, testutils.SameEndpoints(endpoints, expected), "actual and expected endpoints don't match. %+v:%+v", endpoints, expected)
|
||||
}
|
||||
|
||||
func validateAWSZones(t *testing.T, zones map[string]*route53.HostedZone, expected map[string]*route53.HostedZone) {
|
||||
@ -991,33 +1047,23 @@ func TestAWSisAWSAlias(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
target string
|
||||
recordType string
|
||||
alias string
|
||||
expected string
|
||||
alias bool
|
||||
hz string
|
||||
}{
|
||||
{"bar.example.org", endpoint.RecordTypeCNAME, "true", "Z215JYRZR1TBD5"},
|
||||
{"foo.example.org", endpoint.RecordTypeCNAME, "true", ""},
|
||||
{"foo.example.org", endpoint.RecordTypeCNAME, false, ""}, // normal CNAME
|
||||
{"bar.eu-central-1.elb.amazonaws.com", endpoint.RecordTypeCNAME, true, "Z215JYRZR1TBD5"}, // pointing to ELB DNS name
|
||||
{"foobar.example.org", endpoint.RecordTypeCNAME, true, "Z1234567890ABC"}, // HZID retrieved by Route53
|
||||
{"baz.example.org", endpoint.RecordTypeCNAME, true, sameZoneAlias}, // record to be created
|
||||
} {
|
||||
ep := &endpoint.Endpoint{
|
||||
Targets: endpoint.Targets{tc.target},
|
||||
RecordType: tc.recordType,
|
||||
ProviderSpecific: endpoint.ProviderSpecific{
|
||||
endpoint.ProviderSpecificProperty{
|
||||
Name: "alias",
|
||||
Value: tc.alias,
|
||||
},
|
||||
},
|
||||
}
|
||||
addrs := []*endpoint.Endpoint{
|
||||
{
|
||||
DNSName: "foo.example.org",
|
||||
Targets: endpoint.Targets{"foobar.example.org"},
|
||||
},
|
||||
{
|
||||
DNSName: "bar.example.org",
|
||||
Targets: endpoint.Targets{"bar.eu-central-1.elb.amazonaws.com"},
|
||||
},
|
||||
if tc.alias {
|
||||
ep = ep.WithProviderSpecific(providerSpecificAlias, "true")
|
||||
ep = ep.WithProviderSpecific(providerSpecificTargetHostedZone, tc.hz)
|
||||
}
|
||||
assert.Equal(t, tc.expected, isAWSAlias(ep, addrs))
|
||||
assert.Equal(t, tc.hz, isAWSAlias(ep), "%v", tc)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1198,6 +1244,7 @@ func listAWSRecords(t *testing.T, client Route53API, zone string) []*route53.Res
|
||||
recordSets := []*route53.ResourceRecordSet{}
|
||||
require.NoError(t, client.ListResourceRecordSetsPagesWithContext(context.Background(), &route53.ListResourceRecordSetsInput{
|
||||
HostedZoneId: aws.String(zone),
|
||||
MaxItems: aws.String(route53PageSize),
|
||||
}, func(resp *route53.ListResourceRecordSetsOutput, _ bool) bool {
|
||||
recordSets = append(recordSets, resp.ResourceRecordSets...)
|
||||
return true
|
||||
@ -1255,7 +1302,7 @@ func newAWSProvider(t *testing.T, domainFilter endpoint.DomainFilter, zoneIDFilt
|
||||
}
|
||||
|
||||
func newAWSProviderWithTagFilter(t *testing.T, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, zoneTypeFilter provider.ZoneTypeFilter, zoneTagFilter provider.ZoneTagFilter, evaluateTargetHealth, dryRun bool, records []*endpoint.Endpoint) (*AWSProvider, *Route53APIStub) {
|
||||
client := NewRoute53APIStub()
|
||||
client := NewRoute53APIStub(t)
|
||||
|
||||
provider := &AWSProvider{
|
||||
client: client,
|
||||
|
@ -119,7 +119,7 @@ func (p *AzureProvider) Records(ctx context.Context) (endpoints []*endpoint.Endp
|
||||
}
|
||||
targets := extractAzureTargets(&recordSet)
|
||||
if len(targets) == 0 {
|
||||
log.Errorf("Failed to extract targets for '%s' with type '%s'.", name, recordType)
|
||||
log.Debugf("Failed to extract targets for '%s' with type '%s'.", name, recordType)
|
||||
return true
|
||||
}
|
||||
var ttl endpoint.TTL
|
||||
|
6
provider/bluecat/OWNERS
Normal file
6
provider/bluecat/OWNERS
Normal file
@ -0,0 +1,6 @@
|
||||
approvers:
|
||||
- seanmalloy
|
||||
- vinny-sabatini
|
||||
reviewers:
|
||||
- seanmalloy
|
||||
- vinny-sabatini
|
1044
provider/bluecat/bluecat.go
Normal file
1044
provider/bluecat/bluecat.go
Normal file
File diff suppressed because it is too large
Load Diff
480
provider/bluecat/bluecat_test.go
Normal file
480
provider/bluecat/bluecat_test.go
Normal file
@ -0,0 +1,480 @@
|
||||
/*
|
||||
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, zone string) (err error) {
|
||||
*g.mockBluecatHosts = nil
|
||||
return nil
|
||||
}
|
||||
func (g mockGatewayClient) deleteCNAMERecord(name string, zone 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, zone 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, ttl int) BluecatHostRecord {
|
||||
props := "absoluteName=" + fqdn + "|addresses=" + target + "|ttl=" + fmt.Sprint(ttl) + "|"
|
||||
nameParts := strings.Split(fqdn, ".")
|
||||
return BluecatHostRecord{
|
||||
Name: nameParts[0],
|
||||
Properties: props,
|
||||
ID: 3,
|
||||
}
|
||||
}
|
||||
|
||||
func createMockBluecatCNAME(alias, target string, ttl int) BluecatCNAMERecord {
|
||||
props := "absoluteName=" + alias + "|linkedRecordName=" + target + "|ttl=" + fmt.Sprint(ttl) + "|"
|
||||
nameParts := strings.Split(alias, ".")
|
||||
return BluecatCNAMERecord{
|
||||
Name: nameParts[0],
|
||||
Properties: props,
|
||||
}
|
||||
}
|
||||
|
||||
func createMockBluecatTXT(fqdn, txt string) BluecatTXTRecord {
|
||||
return BluecatTXTRecord{
|
||||
Name: fqdn,
|
||||
Properties: 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"},
|
||||
RecordTTL: endpoint.TTL(30),
|
||||
},
|
||||
{
|
||||
DNSName: "nginx.example.com",
|
||||
RecordType: endpoint.RecordTypeA,
|
||||
Targets: endpoint.Targets{"123.123.123.123"},
|
||||
RecordTTL: endpoint.TTL(30),
|
||||
},
|
||||
{
|
||||
DNSName: "whitespace.example.com",
|
||||
RecordType: endpoint.RecordTypeA,
|
||||
Targets: endpoint.Targets{"123.123.123.124"},
|
||||
RecordTTL: endpoint.TTL(30),
|
||||
},
|
||||
{
|
||||
DNSName: "hack.example.com",
|
||||
RecordType: endpoint.RecordTypeCNAME,
|
||||
Targets: endpoint.Targets{"bluecatnetworks.com"},
|
||||
RecordTTL: endpoint.TTL(30),
|
||||
},
|
||||
{
|
||||
DNSName: "wack.example.com",
|
||||
RecordType: endpoint.RecordTypeTXT,
|
||||
Targets: endpoint.Targets{"hello"},
|
||||
Labels: endpoint.Labels{"owner": ""},
|
||||
},
|
||||
{
|
||||
DNSName: "sack.example.com",
|
||||
RecordType: endpoint.RecordTypeTXT,
|
||||
Targets: endpoint.Targets{""},
|
||||
Labels: endpoint.Labels{"owner": ""},
|
||||
},
|
||||
{
|
||||
DNSName: "kdb.example.com",
|
||||
RecordType: endpoint.RecordTypeTXT,
|
||||
Targets: endpoint.Targets{"heritage=external-dns,external-dns/owner=default,external-dns/resource=service/openshift-ingress/router-default"},
|
||||
Labels: endpoint.Labels{"owner": "default"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestBluecatRecords(t *testing.T) {
|
||||
client := mockGatewayClient{
|
||||
mockBluecatZones: &[]BluecatZone{
|
||||
createMockBluecatZone("example.com"),
|
||||
},
|
||||
mockBluecatTXTs: &[]BluecatTXTRecord{
|
||||
createMockBluecatTXT("kdb.example.com", "heritage=external-dns,external-dns/owner=default,external-dns/resource=service/openshift-ingress/router-default"),
|
||||
createMockBluecatTXT("wack.example.com", "hello"),
|
||||
createMockBluecatTXT("sack.example.com", ""),
|
||||
},
|
||||
mockBluecatHosts: &[]BluecatHostRecord{
|
||||
createMockBluecatHostRecord("example.com", "123.123.123.122", 30),
|
||||
createMockBluecatHostRecord("nginx.example.com", "123.123.123.123", 30),
|
||||
createMockBluecatHostRecord("whitespace.example.com", "123.123.123.124", 30),
|
||||
},
|
||||
mockBluecatCNAMEs: &[]BluecatCNAMERecord{
|
||||
createMockBluecatCNAME("hack.example.com", "bluecatnetworks.com", 30),
|
||||
},
|
||||
}
|
||||
|
||||
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", 30),
|
||||
createMockBluecatHostRecord("nginx.example.com", "123.123.123.123", 30),
|
||||
createMockBluecatHostRecord("whitespace.example.com", "123.123.123.124", 30),
|
||||
},
|
||||
mockBluecatCNAMEs: &[]BluecatCNAMERecord{
|
||||
createMockBluecatCNAME("hack.example.com", "bluecatnetworks.com", 30),
|
||||
},
|
||||
mockBluecatTXTs: &[]BluecatTXTRecord{
|
||||
createMockBluecatTXT("kdb.example.com", "heritage=external-dns,external-dns/owner=default,external-dns/resource=service/openshift-ingress/router-default"),
|
||||
createMockBluecatTXT("wack.example.com", "hello"),
|
||||
createMockBluecatTXT("sack.example.com", ""),
|
||||
},
|
||||
}
|
||||
|
||||
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{})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBluecatApplyChangesDeleteWithOwner(t *testing.T) {
|
||||
client := mockGatewayClient{
|
||||
mockBluecatZones: &[]BluecatZone{
|
||||
createMockBluecatZone("example.com"),
|
||||
},
|
||||
mockBluecatHosts: &[]BluecatHostRecord{
|
||||
createMockBluecatHostRecord("example.com", "123.123.123.122", 30),
|
||||
createMockBluecatHostRecord("nginx.example.com", "123.123.123.123", 30),
|
||||
createMockBluecatHostRecord("whitespace.example.com", "123.123.123.124", 30),
|
||||
},
|
||||
mockBluecatCNAMEs: &[]BluecatCNAMERecord{
|
||||
createMockBluecatCNAME("hack.example.com", "bluecatnetworks.com", 30),
|
||||
},
|
||||
mockBluecatTXTs: &[]BluecatTXTRecord{
|
||||
createMockBluecatTXT("kdb.example.com", "heritage=external-dns,external-dns/owner=default,external-dns/resource=service/openshift-ingress/router-default"),
|
||||
createMockBluecatTXT("wack.example.com", "hello"),
|
||||
createMockBluecatTXT("sack.example.com", ""),
|
||||
},
|
||||
}
|
||||
|
||||
provider := newBluecatProvider(
|
||||
endpoint.NewDomainFilter([]string{"example.com"}),
|
||||
provider.NewZoneIDFilter([]string{""}), false, client)
|
||||
|
||||
for _, ti := range tests {
|
||||
for _, ep := range ti.Endpoints {
|
||||
if strings.Contains(ep.Targets.String(), "external-dns") {
|
||||
owner, err := extractOwnerfromTXTRecord(ep.Targets.String())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
t.Logf("Owner %s %s", owner, err)
|
||||
}
|
||||
}
|
||||
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{})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestExpandZones(t *testing.T) {
|
||||
mockZones := []string{"example.com", "nginx.example.com", "hack.example.com"}
|
||||
expected := []string{"zones/com/zones/example/zones/", "zones/com/zones/example/zones/nginx/zones/", "zones/com/zones/example/zones/hack/zones/"}
|
||||
for i := range mockZones {
|
||||
if expandZone(mockZones[i]) != expected[i] {
|
||||
t.Fatalf("%s", expected[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBluecatNewGatewayClient(t *testing.T) {
|
||||
testCookie := http.Cookie{Name: "testCookie", Value: "exampleCookie"}
|
||||
testToken := "exampleToken"
|
||||
testgateWayHost := "exampleHost"
|
||||
testDNSConfiguration := "exampleDNSConfiguration"
|
||||
testView := "testView"
|
||||
testZone := "example.com"
|
||||
testVerify := true
|
||||
|
||||
client := NewGatewayClient(testCookie, testToken, testgateWayHost, testDNSConfiguration, testView, testZone, testVerify)
|
||||
|
||||
if client.Cookie.Value != testCookie.Value || client.Cookie.Name != testCookie.Name || client.Token != testToken || client.Host != testgateWayHost || client.DNSConfiguration != testDNSConfiguration || client.View != testView || client.RootZone != testZone || client.SkipTLSVerify != testVerify {
|
||||
t.Fatal("Client values dont match")
|
||||
}
|
||||
}
|
||||
|
||||
// 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", 30),
|
||||
createMockBluecatHostRecord("nginx.example.com", "123.123.123.123", 30),
|
||||
createMockBluecatHostRecord("whitespace.example.com", "123.123.123.124", 30),
|
||||
},
|
||||
mockBluecatCNAMEs: &[]BluecatCNAMERecord{
|
||||
createMockBluecatCNAME("hack.example.com", "bluecatnetworks.com", 30),
|
||||
},
|
||||
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", 30),
|
||||
}
|
||||
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", 30),
|
||||
}
|
||||
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)
|
||||
}
|
@ -27,11 +27,11 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/nesv/go-dynect/dynect"
|
||||
"github.com/sanyu/dynectsoap/dynectsoap"
|
||||
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
"sigs.k8s.io/external-dns/plan"
|
||||
"sigs.k8s.io/external-dns/provider"
|
||||
dynsoap "sigs.k8s.io/external-dns/provider/dyn/soap"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -250,7 +250,7 @@ func apiRetryLoop(f func() error) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *dynProviderState) allRecordsToEndpoints(records *dynectsoap.GetAllRecordsResponseType) []*endpoint.Endpoint {
|
||||
func (d *dynProviderState) allRecordsToEndpoints(records *dynsoap.GetAllRecordsResponseType) []*endpoint.Endpoint {
|
||||
result := []*endpoint.Endpoint{}
|
||||
//Convert each record to an endpoint
|
||||
|
||||
@ -330,20 +330,23 @@ func (d *dynProviderState) fetchZoneSerial(client *dynect.Client, zone string) (
|
||||
}
|
||||
|
||||
//Use SOAP to fetch all records with a single call
|
||||
func (d *dynProviderState) fetchAllRecordsInZone(zone string) (*dynectsoap.GetAllRecordsResponseType, error) {
|
||||
func (d *dynProviderState) fetchAllRecordsInZone(zone string) (*dynsoap.GetAllRecordsResponseType, error) {
|
||||
var err error
|
||||
client := dynectsoap.NewClient("https://api2.dynect.net/SOAP/")
|
||||
service := dynectsoap.NewDynect(client)
|
||||
|
||||
sessionRequest := dynectsoap.SessionLoginRequestType{
|
||||
service := dynsoap.NewDynectClient("https://api2.dynect.net/SOAP/")
|
||||
|
||||
sessionRequest := dynsoap.SessionLoginRequestType{
|
||||
Customer_name: d.CustomerName,
|
||||
User_name: d.Username,
|
||||
Password: d.Password,
|
||||
Fault_incompat: 0,
|
||||
}
|
||||
resp := dynectsoap.SessionLoginResponseType{}
|
||||
|
||||
var resp *dynsoap.SessionLoginResponseType
|
||||
|
||||
err = apiRetryLoop(func() error {
|
||||
return service.Do(&sessionRequest, &resp)
|
||||
resp, err = service.SessionLogin(&sessionRequest)
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@ -352,46 +355,56 @@ func (d *dynProviderState) fetchAllRecordsInZone(zone string) (*dynectsoap.GetAl
|
||||
|
||||
token := resp.Data.Token
|
||||
|
||||
logoutRequest := dynectsoap.SessionLogoutRequestType{
|
||||
logoutRequest := &dynsoap.SessionLogoutRequestType{
|
||||
Token: token,
|
||||
Fault_incompat: 0,
|
||||
}
|
||||
logoutResponse := dynectsoap.SessionLogoutResponseType{}
|
||||
defer service.Do(&logoutRequest, &logoutResponse)
|
||||
|
||||
req := dynectsoap.GetAllRecordsRequestType{
|
||||
defer service.SessionLogout(logoutRequest)
|
||||
|
||||
req := dynsoap.GetAllRecordsRequestType{
|
||||
Token: token,
|
||||
Zone: zone,
|
||||
Fault_incompat: 0,
|
||||
}
|
||||
records := dynectsoap.GetAllRecordsResponseType{}
|
||||
|
||||
var records = &dynsoap.GetAllRecordsResponseType{}
|
||||
|
||||
err = apiRetryLoop(func() error {
|
||||
return service.Do(&req, &records)
|
||||
records, err = service.GetAllRecords(&req)
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Debugf("Got all Records, status is %s", records.Status)
|
||||
|
||||
if strings.ToLower(records.Status) == "incomplete" {
|
||||
jobRequest := dynectsoap.GetJobRequestType{
|
||||
jobRequest := dynsoap.GetJobRequestType{
|
||||
Token: token,
|
||||
Job_id: records.Job_id,
|
||||
Fault_incompat: 0,
|
||||
}
|
||||
|
||||
jobResults := dynectsoap.GetJobResponseType{}
|
||||
var jobResults = dynsoap.GetJobResponseType{}
|
||||
err = apiRetryLoop(func() error {
|
||||
return service.GetJobRetry(&jobRequest, &jobResults)
|
||||
jobResults, err := service.GetJob(&jobRequest)
|
||||
if strings.ToLower(jobResults.Status) == "incomplete" {
|
||||
return fmt.Errorf("job is incomplete")
|
||||
}
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return jobResults.Data.(*dynectsoap.GetAllRecordsResponseType), nil
|
||||
|
||||
return jobResults.Data.(*dynsoap.GetAllRecordsResponseType), nil
|
||||
}
|
||||
|
||||
return &records, nil
|
||||
return records, nil
|
||||
}
|
||||
|
||||
// buildLinkToRecord build a resource link. The symmetry of the dyn API is used to save
|
||||
|
44
provider/dyn/soap/client.go
Normal file
44
provider/dyn/soap/client.go
Normal file
@ -0,0 +1,44 @@
|
||||
/*
|
||||
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 dynsoap
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/hooklift/gowsdl/soap"
|
||||
)
|
||||
|
||||
// NewDynectClient returns a client with a configured http.Client
|
||||
// The default settings for the http.client are a timeout of
|
||||
// 10 seconds and reading proxy variables from http.ProxyFromEnvironment
|
||||
func NewDynectClient(url string) Dynect {
|
||||
client := &http.Client{
|
||||
Timeout: time.Second * 10,
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
},
|
||||
}
|
||||
soapClient := soap.NewClient(url, soap.WithHTTPClient(client))
|
||||
return NewDynect(soapClient)
|
||||
}
|
||||
|
||||
// NewCustomDynectClient returns a client without a configured http.Client
|
||||
func NewCustomDynectClient(url string, client http.Client) Dynect {
|
||||
soapClient := soap.NewClient(url, soap.WithHTTPClient(&client))
|
||||
return NewDynect(soapClient)
|
||||
}
|
32865
provider/dyn/soap/services.go
Normal file
32865
provider/dyn/soap/services.go
Normal file
File diff suppressed because it is too large
Load Diff
120
provider/gandi/client.go
Normal file
120
provider/gandi/client.go
Normal file
@ -0,0 +1,120 @@
|
||||
/*
|
||||
Copyright 2021 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 gandi
|
||||
|
||||
import (
|
||||
"github.com/go-gandi/go-gandi/domain"
|
||||
"github.com/go-gandi/go-gandi/livedns"
|
||||
)
|
||||
|
||||
type DomainClientAdapter interface {
|
||||
ListDomains() (domains []domain.ListResponse, err error)
|
||||
}
|
||||
|
||||
type domainClient struct {
|
||||
Client *domain.Domain
|
||||
}
|
||||
|
||||
func (p *domainClient) ListDomains() (domains []domain.ListResponse, err error) {
|
||||
return p.Client.ListDomains()
|
||||
}
|
||||
|
||||
func NewDomainClient(client *domain.Domain) DomainClientAdapter {
|
||||
return &domainClient{client}
|
||||
}
|
||||
|
||||
// standardResponse copied from go-gandi/internal/gandi.go
|
||||
type standardResponse struct {
|
||||
Code int `json:"code,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
UUID string `json:"uuid,omitempty"`
|
||||
Object string `json:"object,omitempty"`
|
||||
Cause string `json:"cause,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Errors []standardError `json:"errors,omitempty"`
|
||||
}
|
||||
|
||||
// standardError copied from go-gandi/internal/gandi.go
|
||||
type standardError struct {
|
||||
Location string `json:"location"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type LiveDNSClientAdapter interface {
|
||||
GetDomainRecords(fqdn string) (records []livedns.DomainRecord, err error)
|
||||
CreateDomainRecord(fqdn, name, recordtype string, ttl int, values []string) (response standardResponse, err error)
|
||||
DeleteDomainRecord(fqdn, name, recordtype string) (err error)
|
||||
UpdateDomainRecordByNameAndType(fqdn, name, recordtype string, ttl int, values []string) (response standardResponse, err error)
|
||||
}
|
||||
|
||||
type LiveDNSClient struct {
|
||||
Client *livedns.LiveDNS
|
||||
}
|
||||
|
||||
func NewLiveDNSClient(client *livedns.LiveDNS) LiveDNSClientAdapter {
|
||||
return &LiveDNSClient{client}
|
||||
}
|
||||
|
||||
func (p *LiveDNSClient) GetDomainRecords(fqdn string) (records []livedns.DomainRecord, err error) {
|
||||
return p.Client.GetDomainRecords(fqdn)
|
||||
}
|
||||
|
||||
func (p *LiveDNSClient) CreateDomainRecord(fqdn, name, recordtype string, ttl int, values []string) (response standardResponse, err error) {
|
||||
res, err := p.Client.CreateDomainRecord(fqdn, name, recordtype, ttl, values)
|
||||
if err != nil {
|
||||
return standardResponse{}, err
|
||||
}
|
||||
|
||||
// response needs to be copied as the Standard* structs are internal
|
||||
var errors []standardError
|
||||
for _, e := range res.Errors {
|
||||
errors = append(errors, standardError(e))
|
||||
}
|
||||
return standardResponse{
|
||||
Code: res.Code,
|
||||
Message: res.Message,
|
||||
UUID: res.UUID,
|
||||
Object: res.Object,
|
||||
Cause: res.Cause,
|
||||
Status: res.Status,
|
||||
Errors: errors,
|
||||
}, err
|
||||
}
|
||||
|
||||
func (p *LiveDNSClient) DeleteDomainRecord(fqdn, name, recordtype string) (err error) {
|
||||
return p.Client.DeleteDomainRecord(fqdn, name, recordtype)
|
||||
}
|
||||
|
||||
func (p *LiveDNSClient) UpdateDomainRecordByNameAndType(fqdn, name, recordtype string, ttl int, values []string) (response standardResponse, err error) {
|
||||
res, err := p.Client.UpdateDomainRecordByNameAndType(fqdn, name, recordtype, ttl, values)
|
||||
if err != nil {
|
||||
return standardResponse{}, err
|
||||
}
|
||||
|
||||
// response needs to be copied as the Standard* structs are internal
|
||||
var errors []standardError
|
||||
for _, e := range res.Errors {
|
||||
errors = append(errors, standardError(e))
|
||||
}
|
||||
return standardResponse{
|
||||
Code: res.Code,
|
||||
Message: res.Message,
|
||||
UUID: res.UUID,
|
||||
Object: res.Object,
|
||||
Cause: res.Cause,
|
||||
Status: res.Status,
|
||||
Errors: errors,
|
||||
}, err
|
||||
}
|
268
provider/gandi/gandi.go
Normal file
268
provider/gandi/gandi.go
Normal file
@ -0,0 +1,268 @@
|
||||
/*
|
||||
Copyright 2021 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 gandi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/go-gandi/go-gandi"
|
||||
"github.com/go-gandi/go-gandi/livedns"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
"sigs.k8s.io/external-dns/plan"
|
||||
"sigs.k8s.io/external-dns/provider"
|
||||
)
|
||||
|
||||
const (
|
||||
gandiCreate = "CREATE"
|
||||
gandiDelete = "DELETE"
|
||||
gandiUpdate = "UPDATE"
|
||||
gandiTTL = 600
|
||||
gandiLiveDNSProvider = "livedns"
|
||||
)
|
||||
|
||||
type GandiChanges struct {
|
||||
Action string
|
||||
ZoneName string
|
||||
Record livedns.DomainRecord
|
||||
}
|
||||
|
||||
type GandiProvider struct {
|
||||
provider.BaseProvider
|
||||
LiveDNSClient LiveDNSClientAdapter
|
||||
DomainClient DomainClientAdapter
|
||||
domainFilter endpoint.DomainFilter
|
||||
DryRun bool
|
||||
}
|
||||
|
||||
func NewGandiProvider(ctx context.Context, domainFilter endpoint.DomainFilter, dryRun bool) (*GandiProvider, error) {
|
||||
key, ok := os.LookupEnv("GANDI_KEY")
|
||||
if !ok {
|
||||
return nil, errors.New("no environment variable GANDI_KEY provided")
|
||||
}
|
||||
sharingID, _ := os.LookupEnv("GANDI_SHARING_ID")
|
||||
|
||||
g := gandi.Config{
|
||||
SharingID: sharingID,
|
||||
Debug: false,
|
||||
// dry-run doesn't work but it won't hurt passing the flag
|
||||
DryRun: dryRun,
|
||||
}
|
||||
|
||||
liveDNSClient := gandi.NewLiveDNSClient(key, g)
|
||||
domainClient := gandi.NewDomainClient(key, g)
|
||||
|
||||
gandiProvider := &GandiProvider{
|
||||
LiveDNSClient: NewLiveDNSClient(liveDNSClient),
|
||||
DomainClient: NewDomainClient(domainClient),
|
||||
domainFilter: domainFilter,
|
||||
DryRun: dryRun,
|
||||
}
|
||||
return gandiProvider, nil
|
||||
}
|
||||
|
||||
func (p *GandiProvider) Zones() (zones []string, err error) {
|
||||
availableDomains, err := p.DomainClient.ListDomains()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
zones = []string{}
|
||||
for _, domain := range availableDomains {
|
||||
if !p.domainFilter.Match(domain.FQDN) {
|
||||
log.Debugf("Excluding domain %s by domain-filter", domain.FQDN)
|
||||
continue
|
||||
}
|
||||
|
||||
if domain.NameServer.Current != gandiLiveDNSProvider {
|
||||
log.Debugf("Excluding domain %s, not configured for livedns", domain.FQDN)
|
||||
continue
|
||||
}
|
||||
|
||||
zones = append(zones, domain.FQDN)
|
||||
}
|
||||
return zones, nil
|
||||
}
|
||||
|
||||
func (p *GandiProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
|
||||
liveDNSZones, err := p.Zones()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
endpoints := []*endpoint.Endpoint{}
|
||||
for _, zone := range liveDNSZones {
|
||||
records, err := p.LiveDNSClient.GetDomainRecords(zone)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, r := range records {
|
||||
if provider.SupportedRecordType(r.RrsetType) {
|
||||
name := r.RrsetName + "." + zone
|
||||
|
||||
if r.RrsetName == "@" {
|
||||
name = zone
|
||||
}
|
||||
|
||||
if len(r.RrsetValues) > 1 {
|
||||
return nil, fmt.Errorf("can't handle multiple values for rrset %s", name)
|
||||
}
|
||||
|
||||
endpoints = append(endpoints, endpoint.NewEndpoint(name, r.RrsetType, r.RrsetValues[0]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
func (p *GandiProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
|
||||
combinedChanges := make([]*GandiChanges, 0, len(changes.Create)+len(changes.UpdateNew)+len(changes.Delete))
|
||||
|
||||
combinedChanges = append(combinedChanges, p.newGandiChanges(gandiCreate, changes.Create)...)
|
||||
combinedChanges = append(combinedChanges, p.newGandiChanges(gandiUpdate, changes.UpdateNew)...)
|
||||
combinedChanges = append(combinedChanges, p.newGandiChanges(gandiDelete, changes.Delete)...)
|
||||
|
||||
return p.submitChanges(ctx, combinedChanges)
|
||||
}
|
||||
|
||||
func (p *GandiProvider) submitChanges(ctx context.Context, changes []*GandiChanges) error {
|
||||
if len(changes) == 0 {
|
||||
log.Infof("All records are already up to date")
|
||||
return nil
|
||||
}
|
||||
|
||||
liveDNSDomains, err := p.Zones()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
zoneChanges := p.groupAndFilterByZone(liveDNSDomains, changes)
|
||||
|
||||
for _, changes := range zoneChanges {
|
||||
for _, change := range changes {
|
||||
// Prepare record name
|
||||
recordName := strings.TrimSuffix(change.Record.RrsetName, "."+change.ZoneName)
|
||||
if recordName == change.ZoneName {
|
||||
recordName = "@"
|
||||
}
|
||||
if change.Record.RrsetType == endpoint.RecordTypeCNAME && !strings.HasSuffix(change.Record.RrsetValues[0], ".") {
|
||||
change.Record.RrsetValues[0] += "."
|
||||
}
|
||||
change.Record.RrsetName = recordName
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"record": change.Record.RrsetName,
|
||||
"type": change.Record.RrsetType,
|
||||
"value": change.Record.RrsetValues[0],
|
||||
"ttl": change.Record.RrsetTTL,
|
||||
"action": change.Action,
|
||||
"zone": change.ZoneName,
|
||||
}).Info("Changing record")
|
||||
|
||||
if !p.DryRun {
|
||||
switch change.Action {
|
||||
case gandiCreate:
|
||||
answer, err := p.LiveDNSClient.CreateDomainRecord(
|
||||
change.ZoneName,
|
||||
change.Record.RrsetName,
|
||||
change.Record.RrsetType,
|
||||
change.Record.RrsetTTL,
|
||||
change.Record.RrsetValues,
|
||||
)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"Code": answer.Code,
|
||||
"Message": answer.Message,
|
||||
"Cause": answer.Cause,
|
||||
"Errors": answer.Errors,
|
||||
}).Warning("Create problem")
|
||||
return err
|
||||
}
|
||||
case gandiDelete:
|
||||
err := p.LiveDNSClient.DeleteDomainRecord(change.ZoneName, change.Record.RrsetName, change.Record.RrsetType)
|
||||
if err != nil {
|
||||
log.Warning("Delete problem")
|
||||
return err
|
||||
}
|
||||
case gandiUpdate:
|
||||
answer, err := p.LiveDNSClient.UpdateDomainRecordByNameAndType(
|
||||
change.ZoneName,
|
||||
change.Record.RrsetName,
|
||||
change.Record.RrsetType,
|
||||
change.Record.RrsetTTL,
|
||||
change.Record.RrsetValues,
|
||||
)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"Code": answer.Code,
|
||||
"Message": answer.Message,
|
||||
"Cause": answer.Cause,
|
||||
"Errors": answer.Errors,
|
||||
}).Warning("Update problem")
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *GandiProvider) newGandiChanges(action string, endpoints []*endpoint.Endpoint) []*GandiChanges {
|
||||
changes := make([]*GandiChanges, 0, len(endpoints))
|
||||
ttl := gandiTTL
|
||||
for _, e := range endpoints {
|
||||
if e.RecordTTL.IsConfigured() {
|
||||
ttl = int(e.RecordTTL)
|
||||
}
|
||||
change := &GandiChanges{
|
||||
Action: action,
|
||||
Record: livedns.DomainRecord{
|
||||
RrsetType: e.RecordType,
|
||||
RrsetName: e.DNSName,
|
||||
RrsetValues: e.Targets,
|
||||
RrsetTTL: ttl,
|
||||
},
|
||||
}
|
||||
changes = append(changes, change)
|
||||
}
|
||||
return changes
|
||||
}
|
||||
|
||||
func (p *GandiProvider) groupAndFilterByZone(zones []string, changes []*GandiChanges) map[string][]*GandiChanges {
|
||||
change := make(map[string][]*GandiChanges)
|
||||
zoneNameID := provider.ZoneIDName{}
|
||||
|
||||
for _, z := range zones {
|
||||
zoneNameID.Add(z, z)
|
||||
change[z] = []*GandiChanges{}
|
||||
}
|
||||
|
||||
for _, c := range changes {
|
||||
zoneID, zoneName := zoneNameID.FindZone(c.Record.RrsetName)
|
||||
if zoneName == "" {
|
||||
log.Debugf("Skipping record %s because no hosted domain matching record DNS Name was detected", c.Record.RrsetName)
|
||||
continue
|
||||
}
|
||||
c.ZoneName = zoneName
|
||||
change[zoneID] = append(change[zoneID], c)
|
||||
}
|
||||
return change
|
||||
}
|
759
provider/gandi/gandi_test.go
Normal file
759
provider/gandi/gandi_test.go
Normal file
@ -0,0 +1,759 @@
|
||||
/*
|
||||
Copyright 2021 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 gandi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/go-gandi/go-gandi/domain"
|
||||
"github.com/go-gandi/go-gandi/livedns"
|
||||
"github.com/maxatome/go-testdeep/td"
|
||||
"strings"
|
||||
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
"sigs.k8s.io/external-dns/plan"
|
||||
)
|
||||
|
||||
type MockAction struct {
|
||||
Name string
|
||||
FQDN string
|
||||
Record livedns.DomainRecord
|
||||
}
|
||||
|
||||
type mockGandiClient struct {
|
||||
Actions []MockAction
|
||||
FunctionToFail string
|
||||
RecordsToReturn []livedns.DomainRecord
|
||||
}
|
||||
|
||||
func mockGandiClientNew() *mockGandiClient {
|
||||
return &mockGandiClient{
|
||||
RecordsToReturn: testRecords(),
|
||||
}
|
||||
}
|
||||
|
||||
func mockGandiClientNewWithRecords(recordsToReturn []livedns.DomainRecord) *mockGandiClient {
|
||||
return &mockGandiClient{
|
||||
RecordsToReturn: recordsToReturn,
|
||||
}
|
||||
}
|
||||
|
||||
func mockGandiClientNewWithFailure(functionToFail string) *mockGandiClient {
|
||||
return &mockGandiClient{
|
||||
FunctionToFail: functionToFail,
|
||||
RecordsToReturn: testRecords(),
|
||||
}
|
||||
}
|
||||
|
||||
const domainUriPrefix = "https://api.gandi.net/v5/domain/domains/"
|
||||
const exampleDotComUri = domainUriPrefix + "example.com"
|
||||
const exampleDotNetUri = domainUriPrefix + "example.net"
|
||||
|
||||
func testRecords() []livedns.DomainRecord {
|
||||
return []livedns.DomainRecord{
|
||||
{
|
||||
RrsetType: endpoint.RecordTypeCNAME,
|
||||
RrsetTTL: 600,
|
||||
RrsetName: "@",
|
||||
RrsetHref: exampleDotComUri + "/records/%40/A",
|
||||
RrsetValues: []string{"192.168.0.1"},
|
||||
},
|
||||
{
|
||||
RrsetType: endpoint.RecordTypeCNAME,
|
||||
RrsetTTL: 600,
|
||||
RrsetName: "www",
|
||||
RrsetHref: exampleDotComUri + "/records/www/CNAME",
|
||||
RrsetValues: []string{"lb.example.com"},
|
||||
},
|
||||
{
|
||||
RrsetType: endpoint.RecordTypeA,
|
||||
RrsetTTL: 600,
|
||||
RrsetName: "test",
|
||||
RrsetHref: exampleDotComUri + "/records/test/A",
|
||||
RrsetValues: []string{"192.168.0.2"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Mock all methods
|
||||
|
||||
func (m *mockGandiClient) GetDomainRecords(fqdn string) (records []livedns.DomainRecord, err error) {
|
||||
m.Actions = append(m.Actions, MockAction{
|
||||
Name: "GetDomainRecords",
|
||||
FQDN: fqdn,
|
||||
})
|
||||
if m.FunctionToFail == "GetDomainRecords" {
|
||||
return nil, fmt.Errorf("injected error")
|
||||
}
|
||||
|
||||
return m.RecordsToReturn, err
|
||||
}
|
||||
|
||||
func (m *mockGandiClient) CreateDomainRecord(fqdn, name, recordtype string, ttl int, values []string) (response standardResponse, err error) {
|
||||
m.Actions = append(m.Actions, MockAction{
|
||||
Name: "CreateDomainRecord",
|
||||
FQDN: fqdn,
|
||||
Record: livedns.DomainRecord{
|
||||
RrsetType: recordtype,
|
||||
RrsetTTL: ttl,
|
||||
RrsetName: name,
|
||||
RrsetValues: values,
|
||||
},
|
||||
})
|
||||
if m.FunctionToFail == "CreateDomainRecord" {
|
||||
return standardResponse{}, fmt.Errorf("injected error")
|
||||
}
|
||||
|
||||
return standardResponse{}, nil
|
||||
}
|
||||
|
||||
func (m *mockGandiClient) DeleteDomainRecord(fqdn, name, recordtype string) (err error) {
|
||||
m.Actions = append(m.Actions, MockAction{
|
||||
Name: "DeleteDomainRecord",
|
||||
FQDN: fqdn,
|
||||
Record: livedns.DomainRecord{
|
||||
RrsetType: recordtype,
|
||||
RrsetName: name,
|
||||
},
|
||||
})
|
||||
|
||||
if m.FunctionToFail == "DeleteDomainRecord" {
|
||||
return fmt.Errorf("injected error")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockGandiClient) UpdateDomainRecordByNameAndType(fqdn, name, recordtype string, ttl int, values []string) (response standardResponse, err error) {
|
||||
m.Actions = append(m.Actions, MockAction{
|
||||
Name: "UpdateDomainRecordByNameAndType",
|
||||
FQDN: fqdn,
|
||||
Record: livedns.DomainRecord{
|
||||
RrsetType: recordtype,
|
||||
RrsetTTL: ttl,
|
||||
RrsetName: name,
|
||||
RrsetValues: values,
|
||||
},
|
||||
})
|
||||
|
||||
if m.FunctionToFail == "UpdateDomainRecordByNameAndType" {
|
||||
return standardResponse{}, fmt.Errorf("injected error")
|
||||
}
|
||||
|
||||
return standardResponse{}, nil
|
||||
}
|
||||
|
||||
func (m *mockGandiClient) ListDomains() (domains []domain.ListResponse, err error) {
|
||||
m.Actions = append(m.Actions, MockAction{
|
||||
Name: "ListDomains",
|
||||
})
|
||||
|
||||
if m.FunctionToFail == "ListDomains" {
|
||||
return []domain.ListResponse{}, fmt.Errorf("injected error")
|
||||
}
|
||||
|
||||
return []domain.ListResponse{
|
||||
{
|
||||
FQDN: "example.com",
|
||||
FQDNUnicode: "example.com",
|
||||
Href: exampleDotComUri,
|
||||
ID: "b3e9c271-1c29-4441-97d9-bc021a7ac7c3",
|
||||
NameServer: &domain.NameServerConfig{
|
||||
Current: gandiLiveDNSProvider,
|
||||
},
|
||||
TLD: "com",
|
||||
},
|
||||
{
|
||||
FQDN: "example.net",
|
||||
FQDNUnicode: "example.net",
|
||||
Href: exampleDotNetUri,
|
||||
ID: "dc78c1d8-6143-4edb-93bc-3a20d8bc3570",
|
||||
NameServer: &domain.NameServerConfig{
|
||||
Current: "other",
|
||||
},
|
||||
TLD: "net",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Tests
|
||||
|
||||
func TestNewGandiProvider(t *testing.T) {
|
||||
_ = os.Setenv("GANDI_KEY", "myGandiKey")
|
||||
provider, err := NewGandiProvider(context.Background(), endpoint.NewDomainFilter([]string{"example.com"}), true)
|
||||
if err != nil {
|
||||
t.Errorf("failed : %s", err)
|
||||
}
|
||||
assert.Equal(t, true, provider.DryRun)
|
||||
|
||||
_ = os.Setenv("GANDI_SHARING_ID", "aSharingId")
|
||||
provider, err = NewGandiProvider(context.Background(), endpoint.NewDomainFilter([]string{"example.com"}), false)
|
||||
if err != nil {
|
||||
t.Errorf("failed : %s", err)
|
||||
}
|
||||
assert.Equal(t, false, provider.DryRun)
|
||||
|
||||
_ = os.Unsetenv("GANDI_KEY")
|
||||
_, err = NewGandiProvider(context.Background(), endpoint.NewDomainFilter([]string{"example.com"}), true)
|
||||
if err == nil {
|
||||
t.Errorf("expected to fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGandiProvider_TestData(t *testing.T) {
|
||||
mockedClient := mockGandiClientNew()
|
||||
|
||||
// Check test zone data is ok
|
||||
expectedZonesAnswer := []domain.ListResponse{
|
||||
{
|
||||
FQDN: "example.com",
|
||||
FQDNUnicode: "example.com",
|
||||
Href: exampleDotComUri,
|
||||
ID: "b3e9c271-1c29-4441-97d9-bc021a7ac7c3",
|
||||
NameServer: &domain.NameServerConfig{
|
||||
Current: gandiLiveDNSProvider,
|
||||
},
|
||||
TLD: "com",
|
||||
},
|
||||
{
|
||||
FQDN: "example.net",
|
||||
FQDNUnicode: "example.net",
|
||||
Href: exampleDotNetUri,
|
||||
ID: "dc78c1d8-6143-4edb-93bc-3a20d8bc3570",
|
||||
NameServer: &domain.NameServerConfig{
|
||||
Current: "other",
|
||||
},
|
||||
TLD: "net",
|
||||
},
|
||||
}
|
||||
|
||||
testingZonesAnswer, err := mockedClient.ListDomains()
|
||||
if err != nil {
|
||||
t.Errorf("should not fail, %s", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(expectedZonesAnswer, testingZonesAnswer) {
|
||||
t.Errorf("should be equal, %s", err)
|
||||
}
|
||||
|
||||
// Check test record data is ok
|
||||
expectedRecordsAnswer := []livedns.DomainRecord{
|
||||
{
|
||||
RrsetType: endpoint.RecordTypeCNAME,
|
||||
RrsetTTL: 600,
|
||||
RrsetName: "@",
|
||||
RrsetHref: exampleDotComUri + "/records/%40/A",
|
||||
RrsetValues: []string{"192.168.0.1"},
|
||||
},
|
||||
{
|
||||
RrsetType: endpoint.RecordTypeCNAME,
|
||||
RrsetTTL: 600,
|
||||
RrsetName: "www",
|
||||
RrsetHref: exampleDotComUri + "/records/www/CNAME",
|
||||
RrsetValues: []string{"lb.example.com"},
|
||||
},
|
||||
{
|
||||
RrsetType: endpoint.RecordTypeA,
|
||||
RrsetTTL: 600,
|
||||
RrsetName: "test",
|
||||
RrsetHref: exampleDotComUri + "/records/test/A",
|
||||
RrsetValues: []string{"192.168.0.2"},
|
||||
},
|
||||
}
|
||||
|
||||
testingRecordsAnswer, err := mockedClient.GetDomainRecords("example.com")
|
||||
if err != nil {
|
||||
t.Errorf("should not fail, %s", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(expectedRecordsAnswer, testingRecordsAnswer) {
|
||||
t.Errorf("should be equal, %s", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestGandiProvider_Records(t *testing.T) {
|
||||
mockedClient := mockGandiClientNew()
|
||||
|
||||
mockedProvider := &GandiProvider{
|
||||
DomainClient: mockedClient,
|
||||
LiveDNSClient: mockedClient,
|
||||
}
|
||||
|
||||
expectedActions := []MockAction{
|
||||
{
|
||||
Name: "ListDomains",
|
||||
},
|
||||
{
|
||||
Name: "GetDomainRecords",
|
||||
FQDN: "example.com",
|
||||
},
|
||||
}
|
||||
|
||||
endpoints, err := mockedProvider.Records(context.Background())
|
||||
if err != nil {
|
||||
t.Errorf("should not fail, %s", err)
|
||||
}
|
||||
assert.Equal(t, 3, len(endpoints))
|
||||
fmt.Printf("%+v\n", endpoints[0].DNSName)
|
||||
assert.Equal(t, "example.com", endpoints[0].DNSName)
|
||||
assert.Equal(t, endpoint.RecordTypeCNAME, endpoints[0].RecordType)
|
||||
td.Cmp(t, expectedActions, mockedClient.Actions)
|
||||
}
|
||||
|
||||
func TestGandiProvider_RecordsAppliesDomainFilter(t *testing.T) {
|
||||
|
||||
mockedClient := mockGandiClientNew()
|
||||
|
||||
mockedProvider := &GandiProvider{
|
||||
DomainClient: mockedClient,
|
||||
LiveDNSClient: mockedClient,
|
||||
domainFilter: endpoint.NewDomainFilterWithExclusions([]string{}, []string{"example.com"}),
|
||||
}
|
||||
|
||||
expectedActions := []MockAction{
|
||||
{
|
||||
Name: "ListDomains",
|
||||
},
|
||||
}
|
||||
|
||||
endpoints, err := mockedProvider.Records(context.Background())
|
||||
if err != nil {
|
||||
t.Errorf("should not fail, %s", err)
|
||||
}
|
||||
assert.Equal(t, 0, len(endpoints))
|
||||
td.Cmp(t, expectedActions, mockedClient.Actions)
|
||||
}
|
||||
|
||||
func TestGandiProvider_RecordsErrorOnMultipleValues(t *testing.T) {
|
||||
|
||||
mockedClient := mockGandiClientNewWithRecords([]livedns.DomainRecord{
|
||||
{
|
||||
RrsetValues: []string{"foo", "bar"},
|
||||
RrsetType: endpoint.RecordTypeCNAME,
|
||||
},
|
||||
})
|
||||
|
||||
mockedProvider := &GandiProvider{
|
||||
DomainClient: mockedClient,
|
||||
LiveDNSClient: mockedClient,
|
||||
}
|
||||
|
||||
expectedActions := []MockAction{
|
||||
{
|
||||
Name: "ListDomains",
|
||||
},
|
||||
{
|
||||
Name: "GetDomainRecords",
|
||||
FQDN: "example.com",
|
||||
},
|
||||
}
|
||||
|
||||
endpoints, err := mockedProvider.Records(context.Background())
|
||||
if err == nil {
|
||||
t.Errorf("expected to fail")
|
||||
}
|
||||
assert.Equal(t, 0, len(endpoints))
|
||||
assert.True(t, strings.HasPrefix(err.Error(), "can't handle multiple values for rrset"))
|
||||
td.Cmp(t, expectedActions, mockedClient.Actions)
|
||||
}
|
||||
|
||||
func TestGandiProvider_ApplyChangesEmpty(t *testing.T) {
|
||||
changes := &plan.Changes{}
|
||||
mockedClient := mockGandiClientNew()
|
||||
mockedProvider := &GandiProvider{
|
||||
DomainClient: mockedClient,
|
||||
LiveDNSClient: mockedClient,
|
||||
}
|
||||
|
||||
err := mockedProvider.ApplyChanges(context.Background(), changes)
|
||||
if err != nil {
|
||||
t.Errorf("should not fail, %s", err)
|
||||
}
|
||||
|
||||
if mockedClient.Actions != nil {
|
||||
t.Error("expected no changes")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGandiProvider_ApplyChanges(t *testing.T) {
|
||||
changes := &plan.Changes{}
|
||||
mockedClient := mockGandiClientNew()
|
||||
mockedProvider := &GandiProvider{
|
||||
DomainClient: mockedClient,
|
||||
LiveDNSClient: mockedClient,
|
||||
}
|
||||
|
||||
changes.Create = []*endpoint.Endpoint{{DNSName: "test2.example.com", Targets: endpoint.Targets{"target"}, RecordType: "A", RecordTTL: 666}}
|
||||
changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "test3.example.com", Targets: endpoint.Targets{"target-new"}, RecordType: "A", RecordTTL: 777}}
|
||||
changes.Delete = []*endpoint.Endpoint{{DNSName: "test4.example.com", Targets: endpoint.Targets{"target-other"}, RecordType: "A"}}
|
||||
|
||||
err := mockedProvider.ApplyChanges(context.Background(), changes)
|
||||
if err != nil {
|
||||
t.Errorf("should not fail, %s", err)
|
||||
}
|
||||
|
||||
td.Cmp(t, mockedClient.Actions, []MockAction{
|
||||
{
|
||||
Name: "ListDomains",
|
||||
},
|
||||
{
|
||||
Name: "CreateDomainRecord",
|
||||
FQDN: "example.com",
|
||||
Record: livedns.DomainRecord{
|
||||
RrsetType: endpoint.RecordTypeA,
|
||||
RrsetName: "test2",
|
||||
RrsetValues: []string{"target"},
|
||||
RrsetTTL: 666,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "UpdateDomainRecordByNameAndType",
|
||||
FQDN: "example.com",
|
||||
Record: livedns.DomainRecord{
|
||||
RrsetType: endpoint.RecordTypeA,
|
||||
RrsetName: "test3",
|
||||
RrsetValues: []string{"target-new"},
|
||||
RrsetTTL: 777,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "DeleteDomainRecord",
|
||||
FQDN: "example.com",
|
||||
Record: livedns.DomainRecord{
|
||||
RrsetType: endpoint.RecordTypeA,
|
||||
RrsetName: "test4",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestGandiProvider_ApplyChangesSkipsNonManaged(t *testing.T) {
|
||||
changes := &plan.Changes{}
|
||||
mockedClient := mockGandiClientNew()
|
||||
mockedProvider := &GandiProvider{
|
||||
DomainClient: mockedClient,
|
||||
LiveDNSClient: mockedClient,
|
||||
}
|
||||
|
||||
changes.Create = []*endpoint.Endpoint{{DNSName: "example.net", Targets: endpoint.Targets{"target"}}}
|
||||
changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "test.example.net", Targets: endpoint.Targets{"target-new"}, RecordType: "A", RecordTTL: 777}}
|
||||
changes.Delete = []*endpoint.Endpoint{{DNSName: "test2.example.net", Targets: endpoint.Targets{"target"}, RecordType: "A"}}
|
||||
|
||||
err := mockedProvider.ApplyChanges(context.Background(), changes)
|
||||
if err != nil {
|
||||
t.Errorf("should not fail, %s", err)
|
||||
}
|
||||
|
||||
td.Cmp(t, mockedClient.Actions, []MockAction{
|
||||
{
|
||||
Name: "ListDomains",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestGandiProvider_ApplyChangesCreateUpdateCname(t *testing.T) {
|
||||
changes := &plan.Changes{}
|
||||
mockedClient := mockGandiClientNew()
|
||||
mockedProvider := &GandiProvider{
|
||||
DomainClient: mockedClient,
|
||||
LiveDNSClient: mockedClient,
|
||||
}
|
||||
|
||||
changes.Create = []*endpoint.Endpoint{
|
||||
{DNSName: "test-cname.example.com", Targets: endpoint.Targets{"target"}, RecordTTL: 666, RecordType: "CNAME"},
|
||||
}
|
||||
changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "test-cname2.example.com", Targets: endpoint.Targets{"target-new"}, RecordType: "CNAME", RecordTTL: 777}}
|
||||
|
||||
err := mockedProvider.ApplyChanges(context.Background(), changes)
|
||||
if err != nil {
|
||||
t.Errorf("should not fail, %s", err)
|
||||
}
|
||||
|
||||
td.Cmp(t, mockedClient.Actions, []MockAction{
|
||||
{
|
||||
Name: "ListDomains",
|
||||
},
|
||||
{
|
||||
Name: "CreateDomainRecord",
|
||||
FQDN: "example.com",
|
||||
Record: livedns.DomainRecord{
|
||||
RrsetType: endpoint.RecordTypeCNAME,
|
||||
RrsetName: "test-cname",
|
||||
RrsetValues: []string{"target."},
|
||||
RrsetTTL: 666,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "UpdateDomainRecordByNameAndType",
|
||||
FQDN: "example.com",
|
||||
Record: livedns.DomainRecord{
|
||||
RrsetType: endpoint.RecordTypeCNAME,
|
||||
RrsetName: "test-cname2",
|
||||
RrsetValues: []string{"target-new."},
|
||||
RrsetTTL: 777,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestGandiProvider_ApplyChangesCreateEmpty(t *testing.T) {
|
||||
changes := &plan.Changes{}
|
||||
mockedClient := mockGandiClientNew()
|
||||
mockedProvider := &GandiProvider{
|
||||
DomainClient: mockedClient,
|
||||
LiveDNSClient: mockedClient,
|
||||
}
|
||||
|
||||
changes.Create = []*endpoint.Endpoint{
|
||||
{DNSName: "example.com", Targets: endpoint.Targets{"target"}, RecordTTL: 666, RecordType: "A"},
|
||||
}
|
||||
changes.UpdateNew = []*endpoint.Endpoint{}
|
||||
|
||||
err := mockedProvider.ApplyChanges(context.Background(), changes)
|
||||
if err != nil {
|
||||
t.Errorf("should not fail, %s", err)
|
||||
}
|
||||
|
||||
td.Cmp(t, mockedClient.Actions, []MockAction{
|
||||
{
|
||||
Name: "ListDomains",
|
||||
},
|
||||
{
|
||||
Name: "CreateDomainRecord",
|
||||
FQDN: "example.com",
|
||||
Record: livedns.DomainRecord{
|
||||
RrsetType: endpoint.RecordTypeA,
|
||||
RrsetName: "@",
|
||||
RrsetValues: []string{"target"},
|
||||
RrsetTTL: 666,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestGandiProvider_ApplyChangesRespectsDryRun(t *testing.T) {
|
||||
changes := &plan.Changes{}
|
||||
mockedClient := mockGandiClientNew()
|
||||
mockedProvider := &GandiProvider{
|
||||
DomainClient: mockedClient,
|
||||
LiveDNSClient: mockedClient,
|
||||
DryRun: true,
|
||||
}
|
||||
|
||||
changes.Create = []*endpoint.Endpoint{
|
||||
{DNSName: "foo.example.com", Targets: endpoint.Targets{"target"}, RecordTTL: 666, RecordType: "A"},
|
||||
}
|
||||
changes.UpdateNew = []*endpoint.Endpoint{
|
||||
{DNSName: "bar.example.com", Targets: endpoint.Targets{"target"}, RecordTTL: 666, RecordType: "A"},
|
||||
}
|
||||
changes.Delete = []*endpoint.Endpoint{
|
||||
{DNSName: "baz.example.com", Targets: endpoint.Targets{"target"}, RecordTTL: 666, RecordType: "A"},
|
||||
}
|
||||
|
||||
err := mockedProvider.ApplyChanges(context.Background(), changes)
|
||||
if err != nil {
|
||||
t.Errorf("should not fail, %s", err)
|
||||
}
|
||||
|
||||
td.Cmp(t, mockedClient.Actions, []MockAction{
|
||||
{
|
||||
Name: "ListDomains",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestGandiProvider_ApplyChangesErrorListDomains(t *testing.T) {
|
||||
changes := &plan.Changes{}
|
||||
mockedClient := mockGandiClientNewWithFailure("ListDomains")
|
||||
mockedProvider := &GandiProvider{
|
||||
DomainClient: mockedClient,
|
||||
LiveDNSClient: mockedClient,
|
||||
}
|
||||
|
||||
changes.Create = []*endpoint.Endpoint{
|
||||
{DNSName: "foo.example.com", Targets: endpoint.Targets{"target"}, RecordTTL: 666, RecordType: "A"},
|
||||
}
|
||||
changes.UpdateNew = []*endpoint.Endpoint{
|
||||
{DNSName: "bar.example.com", Targets: endpoint.Targets{"target"}, RecordTTL: 666, RecordType: "A"},
|
||||
}
|
||||
changes.Delete = []*endpoint.Endpoint{
|
||||
{DNSName: "baz.example.com", Targets: endpoint.Targets{"target"}, RecordTTL: 666, RecordType: "A"},
|
||||
}
|
||||
|
||||
err := mockedProvider.ApplyChanges(context.Background(), changes)
|
||||
if err == nil {
|
||||
t.Error("should have failed")
|
||||
}
|
||||
|
||||
td.Cmp(t, mockedClient.Actions, []MockAction{
|
||||
{
|
||||
Name: "ListDomains",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestGandiProvider_ApplyChangesErrorCreate(t *testing.T) {
|
||||
changes := &plan.Changes{}
|
||||
mockedClient := mockGandiClientNewWithFailure("CreateDomainRecord")
|
||||
mockedProvider := &GandiProvider{
|
||||
DomainClient: mockedClient,
|
||||
LiveDNSClient: mockedClient,
|
||||
}
|
||||
|
||||
changes.Create = []*endpoint.Endpoint{
|
||||
{DNSName: "foo.example.com", Targets: endpoint.Targets{"target"}, RecordTTL: 666, RecordType: "A"},
|
||||
}
|
||||
changes.UpdateNew = []*endpoint.Endpoint{
|
||||
{DNSName: "bar.example.com", Targets: endpoint.Targets{"target"}, RecordTTL: 666, RecordType: "A"},
|
||||
}
|
||||
changes.Delete = []*endpoint.Endpoint{
|
||||
{DNSName: "baz.example.com", Targets: endpoint.Targets{"target"}, RecordTTL: 666, RecordType: "A"},
|
||||
}
|
||||
|
||||
err := mockedProvider.ApplyChanges(context.Background(), changes)
|
||||
if err == nil {
|
||||
t.Error("should have failed")
|
||||
}
|
||||
|
||||
td.Cmp(t, mockedClient.Actions, []MockAction{
|
||||
{
|
||||
Name: "ListDomains",
|
||||
},
|
||||
{
|
||||
Name: "CreateDomainRecord",
|
||||
FQDN: "example.com",
|
||||
Record: livedns.DomainRecord{
|
||||
RrsetType: endpoint.RecordTypeA,
|
||||
RrsetName: "foo",
|
||||
RrsetValues: []string{"target"},
|
||||
RrsetTTL: 666,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestGandiProvider_ApplyChangesErrorUpdate(t *testing.T) {
|
||||
changes := &plan.Changes{}
|
||||
mockedClient := mockGandiClientNewWithFailure("UpdateDomainRecordByNameAndType")
|
||||
mockedProvider := &GandiProvider{
|
||||
DomainClient: mockedClient,
|
||||
LiveDNSClient: mockedClient,
|
||||
}
|
||||
|
||||
changes.Create = []*endpoint.Endpoint{
|
||||
{DNSName: "foo.example.com", Targets: endpoint.Targets{"target"}, RecordTTL: 666, RecordType: "A"},
|
||||
}
|
||||
changes.UpdateNew = []*endpoint.Endpoint{
|
||||
{DNSName: "bar.example.com", Targets: endpoint.Targets{"target"}, RecordTTL: 666, RecordType: "A"},
|
||||
}
|
||||
changes.Delete = []*endpoint.Endpoint{
|
||||
{DNSName: "baz.example.com", Targets: endpoint.Targets{"target"}, RecordTTL: 666, RecordType: "A"},
|
||||
}
|
||||
|
||||
err := mockedProvider.ApplyChanges(context.Background(), changes)
|
||||
if err == nil {
|
||||
t.Error("should have failed")
|
||||
}
|
||||
|
||||
td.Cmp(t, mockedClient.Actions, []MockAction{
|
||||
{
|
||||
Name: "ListDomains",
|
||||
},
|
||||
{
|
||||
Name: "CreateDomainRecord",
|
||||
FQDN: "example.com",
|
||||
Record: livedns.DomainRecord{
|
||||
RrsetType: endpoint.RecordTypeA,
|
||||
RrsetName: "foo",
|
||||
RrsetValues: []string{"target"},
|
||||
RrsetTTL: 666,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "UpdateDomainRecordByNameAndType",
|
||||
FQDN: "example.com",
|
||||
Record: livedns.DomainRecord{
|
||||
RrsetType: endpoint.RecordTypeA,
|
||||
RrsetName: "bar",
|
||||
RrsetValues: []string{"target"},
|
||||
RrsetTTL: 666,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestGandiProvider_ApplyChangesErrorDelete(t *testing.T) {
|
||||
changes := &plan.Changes{}
|
||||
mockedClient := mockGandiClientNewWithFailure("DeleteDomainRecord")
|
||||
mockedProvider := &GandiProvider{
|
||||
DomainClient: mockedClient,
|
||||
LiveDNSClient: mockedClient,
|
||||
}
|
||||
|
||||
changes.Create = []*endpoint.Endpoint{
|
||||
{DNSName: "foo.example.com", Targets: endpoint.Targets{"target"}, RecordTTL: 666, RecordType: "A"},
|
||||
}
|
||||
changes.UpdateNew = []*endpoint.Endpoint{
|
||||
{DNSName: "bar.example.com", Targets: endpoint.Targets{"target"}, RecordTTL: 666, RecordType: "A"},
|
||||
}
|
||||
changes.Delete = []*endpoint.Endpoint{
|
||||
{DNSName: "baz.example.com", Targets: endpoint.Targets{"target"}, RecordTTL: 666, RecordType: "A"},
|
||||
}
|
||||
|
||||
err := mockedProvider.ApplyChanges(context.Background(), changes)
|
||||
if err == nil {
|
||||
t.Error("should have failed")
|
||||
}
|
||||
|
||||
td.Cmp(t, mockedClient.Actions, []MockAction{
|
||||
{
|
||||
Name: "ListDomains",
|
||||
},
|
||||
{
|
||||
Name: "CreateDomainRecord",
|
||||
FQDN: "example.com",
|
||||
Record: livedns.DomainRecord{
|
||||
RrsetType: endpoint.RecordTypeA,
|
||||
RrsetName: "foo",
|
||||
RrsetValues: []string{"target"},
|
||||
RrsetTTL: 666,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "UpdateDomainRecordByNameAndType",
|
||||
FQDN: "example.com",
|
||||
Record: livedns.DomainRecord{
|
||||
RrsetType: endpoint.RecordTypeA,
|
||||
RrsetName: "bar",
|
||||
RrsetValues: []string{"target"},
|
||||
RrsetTTL: 666,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "DeleteDomainRecord",
|
||||
FQDN: "example.com",
|
||||
Record: livedns.DomainRecord{
|
||||
RrsetType: endpoint.RecordTypeA,
|
||||
RrsetName: "baz",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
323
provider/godaddy/client.go
Normal file
323
provider/godaddy/client.go
Normal file
@ -0,0 +1,323 @@
|
||||
/*
|
||||
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 godaddy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"sigs.k8s.io/external-dns/pkg/apis/externaldns"
|
||||
)
|
||||
|
||||
// DefaultTimeout api requests after 180s
|
||||
const DefaultTimeout = 180 * time.Second
|
||||
|
||||
// Errors
|
||||
var (
|
||||
ErrAPIDown = errors.New("godaddy: the GoDaddy API is down")
|
||||
)
|
||||
|
||||
// APIError error
|
||||
type APIError struct {
|
||||
Code string
|
||||
Message string
|
||||
}
|
||||
|
||||
func (err *APIError) Error() string {
|
||||
return fmt.Sprintf("Error %s: %q", err.Code, err.Message)
|
||||
}
|
||||
|
||||
// Logger is the interface that should be implemented for loggers that wish to
|
||||
// log HTTP requests and HTTP responses.
|
||||
type Logger interface {
|
||||
// LogRequest logs an HTTP request.
|
||||
LogRequest(*http.Request)
|
||||
|
||||
// LogResponse logs an HTTP response.
|
||||
LogResponse(*http.Response)
|
||||
}
|
||||
|
||||
// Client represents a client to call the GoDaddy API
|
||||
type Client struct {
|
||||
// APIKey holds the Application key
|
||||
APIKey string
|
||||
|
||||
// APISecret holds the Application secret key
|
||||
APISecret string
|
||||
|
||||
// API endpoint
|
||||
APIEndPoint string
|
||||
|
||||
// Client is the underlying HTTP client used to run the requests. It may be overloaded but a default one is instanciated in ``NewClient`` by default.
|
||||
Client *http.Client
|
||||
|
||||
// Logger is used to log HTTP requests and responses.
|
||||
Logger Logger
|
||||
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
// GDErrorField describe the error reason
|
||||
type GDErrorField struct {
|
||||
Code string `json:"code,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
PathRelated string `json:"pathRelated,omitempty"`
|
||||
}
|
||||
|
||||
// GDErrorResponse is the body response when an API call fails
|
||||
type GDErrorResponse struct {
|
||||
Code string `json:"code"`
|
||||
Fields []GDErrorField `json:"fields,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
func (r GDErrorResponse) String() string {
|
||||
if b, err := json.Marshal(r); err == nil {
|
||||
return string(b)
|
||||
}
|
||||
|
||||
return "<error>"
|
||||
}
|
||||
|
||||
// NewClient represents a new client to call the API
|
||||
func NewClient(useOTE bool, apiKey, apiSecret string) (*Client, error) {
|
||||
var endpoint string
|
||||
|
||||
if useOTE {
|
||||
endpoint = " https://api.ote-godaddy.com"
|
||||
} else {
|
||||
endpoint = "https://api.godaddy.com"
|
||||
}
|
||||
|
||||
client := Client{
|
||||
APIKey: apiKey,
|
||||
APISecret: apiSecret,
|
||||
APIEndPoint: endpoint,
|
||||
Client: &http.Client{},
|
||||
Timeout: DefaultTimeout,
|
||||
}
|
||||
|
||||
// Get and check the configuration
|
||||
if err := client.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &client, nil
|
||||
}
|
||||
|
||||
//
|
||||
// Common request wrappers
|
||||
//
|
||||
|
||||
// Get is a wrapper for the GET method
|
||||
func (c *Client) Get(url string, resType interface{}) error {
|
||||
return c.CallAPI("GET", url, nil, resType, true)
|
||||
}
|
||||
|
||||
// Patch is a wrapper for the POST method
|
||||
func (c *Client) Patch(url string, reqBody, resType interface{}) error {
|
||||
return c.CallAPI("PATCH", url, reqBody, resType, true)
|
||||
}
|
||||
|
||||
// Post is a wrapper for the POST method
|
||||
func (c *Client) Post(url string, reqBody, resType interface{}) error {
|
||||
return c.CallAPI("POST", url, reqBody, resType, true)
|
||||
}
|
||||
|
||||
// Put is a wrapper for the PUT method
|
||||
func (c *Client) Put(url string, reqBody, resType interface{}) error {
|
||||
return c.CallAPI("PUT", url, reqBody, resType, true)
|
||||
}
|
||||
|
||||
// Delete is a wrapper for the DELETE method
|
||||
func (c *Client) Delete(url string, resType interface{}) error {
|
||||
return c.CallAPI("DELETE", url, nil, resType, true)
|
||||
}
|
||||
|
||||
// GetWithContext is a wrapper for the GET method
|
||||
func (c *Client) GetWithContext(ctx context.Context, url string, resType interface{}) error {
|
||||
return c.CallAPIWithContext(ctx, "GET", url, nil, resType, true)
|
||||
}
|
||||
|
||||
// PatchWithContext is a wrapper for the POST method
|
||||
func (c *Client) PatchWithContext(ctx context.Context, url string, reqBody, resType interface{}) error {
|
||||
return c.CallAPIWithContext(ctx, "PATCH", url, reqBody, resType, true)
|
||||
}
|
||||
|
||||
// PostWithContext is a wrapper for the POST method
|
||||
func (c *Client) PostWithContext(ctx context.Context, url string, reqBody, resType interface{}) error {
|
||||
return c.CallAPIWithContext(ctx, "POST", url, reqBody, resType, true)
|
||||
}
|
||||
|
||||
// PutWithContext is a wrapper for the PUT method
|
||||
func (c *Client) PutWithContext(ctx context.Context, url string, reqBody, resType interface{}) error {
|
||||
return c.CallAPIWithContext(ctx, "PUT", url, reqBody, resType, true)
|
||||
}
|
||||
|
||||
// DeleteWithContext is a wrapper for the DELETE method
|
||||
func (c *Client) DeleteWithContext(ctx context.Context, url string, resType interface{}) error {
|
||||
return c.CallAPIWithContext(ctx, "DELETE", url, nil, resType, true)
|
||||
}
|
||||
|
||||
// NewRequest returns a new HTTP request
|
||||
func (c *Client) NewRequest(method, path string, reqBody interface{}, needAuth bool) (*http.Request, error) {
|
||||
var body []byte
|
||||
var err error
|
||||
|
||||
if reqBody != nil {
|
||||
body, err = json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
target := fmt.Sprintf("%s%s", c.APIEndPoint, path)
|
||||
req, err := http.NewRequest(method, target, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Inject headers
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json;charset=utf-8")
|
||||
}
|
||||
req.Header.Set("Authorization", fmt.Sprintf("sso-key %s:%s", c.APIKey, c.APISecret))
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("User-Agent", "ExternalDNS/"+externaldns.Version)
|
||||
|
||||
// Send the request with requested timeout
|
||||
c.Client.Timeout = c.Timeout
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// Do sends an HTTP request and returns an HTTP response
|
||||
func (c *Client) Do(req *http.Request) (*http.Response, error) {
|
||||
if c.Logger != nil {
|
||||
c.Logger.LogRequest(req)
|
||||
}
|
||||
resp, err := c.Client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if c.Logger != nil {
|
||||
c.Logger.LogResponse(resp)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// CallAPI is the lowest level call helper. If needAuth is true,
|
||||
// inject authentication headers and sign the request.
|
||||
//
|
||||
// Request signature is a sha1 hash on following fields, joined by '+':
|
||||
// - applicationSecret (from Client instance)
|
||||
// - consumerKey (from Client instance)
|
||||
// - capitalized method (from arguments)
|
||||
// - full request url, including any query string argument
|
||||
// - full serialized request body
|
||||
// - server current time (takes time delta into account)
|
||||
//
|
||||
// Call will automatically assemble the target url from the endpoint
|
||||
// configured in the client instance and the path argument. If the reqBody
|
||||
// argument is not nil, it will also serialize it as json and inject
|
||||
// the required Content-Type header.
|
||||
//
|
||||
// If everything went fine, unmarshall response into resType and return nil
|
||||
// otherwise, return the error
|
||||
func (c *Client) CallAPI(method, path string, reqBody, resType interface{}, needAuth bool) error {
|
||||
return c.CallAPIWithContext(context.Background(), method, path, reqBody, resType, needAuth)
|
||||
}
|
||||
|
||||
// CallAPIWithContext is the lowest level call helper. If needAuth is true,
|
||||
// inject authentication headers and sign the request.
|
||||
//
|
||||
// Request signature is a sha1 hash on following fields, joined by '+':
|
||||
// - applicationSecret (from Client instance)
|
||||
// - consumerKey (from Client instance)
|
||||
// - capitalized method (from arguments)
|
||||
// - full request url, including any query string argument
|
||||
// - full serialized request body
|
||||
// - server current time (takes time delta into account)
|
||||
//
|
||||
// Context is used by http.Client to handle context cancelation
|
||||
//
|
||||
// Call will automatically assemble the target url from the endpoint
|
||||
// configured in the client instance and the path argument. If the reqBody
|
||||
// argument is not nil, it will also serialize it as json and inject
|
||||
// the required Content-Type header.
|
||||
//
|
||||
// If everything went fine, unmarshall response into resType and return nil
|
||||
// otherwise, return the error
|
||||
func (c *Client) CallAPIWithContext(ctx context.Context, method, path string, reqBody, resType interface{}, needAuth bool) error {
|
||||
req, err := c.NewRequest(method, path, reqBody, needAuth)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
response, err := c.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.UnmarshalResponse(response, resType)
|
||||
}
|
||||
|
||||
// UnmarshalResponse checks the response and unmarshals it into the response
|
||||
// type if needed Helper function, called from CallAPI
|
||||
func (c *Client) UnmarshalResponse(response *http.Response, resType interface{}) error {
|
||||
// Read all the response body
|
||||
defer response.Body.Close()
|
||||
body, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// < 200 && >= 300 : API error
|
||||
if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices {
|
||||
apiError := &APIError{
|
||||
Code: fmt.Sprintf("HTTPStatus: %d", response.StatusCode),
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(body, apiError); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return apiError
|
||||
}
|
||||
|
||||
// Nothing to unmarshal
|
||||
if len(body) == 0 || resType == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return json.Unmarshal(body, &resType)
|
||||
}
|
||||
|
||||
func (c *Client) validate() error {
|
||||
var response interface{}
|
||||
|
||||
if err := c.Get("/v1/domains?statuses=ACTIVE", response); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
558
provider/godaddy/godaddy.go
Normal file
558
provider/godaddy/godaddy.go
Normal file
@ -0,0 +1,558 @@
|
||||
/*
|
||||
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 godaddy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
"sigs.k8s.io/external-dns/plan"
|
||||
"sigs.k8s.io/external-dns/provider"
|
||||
)
|
||||
|
||||
const (
|
||||
gdMinimalTTL = 600
|
||||
gdCreate = 0
|
||||
gdUpdate = 1
|
||||
gdDelete = 2
|
||||
)
|
||||
|
||||
var actionNames = []string{
|
||||
"create",
|
||||
"update",
|
||||
"delete",
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrRecordToMutateNotFound when ApplyChange has to update/delete and didn't found the record in the existing zone (Change with no record ID)
|
||||
ErrRecordToMutateNotFound = errors.New("record to mutate not found in current zone")
|
||||
)
|
||||
|
||||
type gdClient interface {
|
||||
Patch(string, interface{}, interface{}) error
|
||||
Post(string, interface{}, interface{}) error
|
||||
Put(string, interface{}, interface{}) error
|
||||
Get(string, interface{}) error
|
||||
Delete(string, interface{}) error
|
||||
}
|
||||
|
||||
// GDProvider declare GoDaddy provider
|
||||
type GDProvider struct {
|
||||
provider.BaseProvider
|
||||
|
||||
domainFilter endpoint.DomainFilter
|
||||
client gdClient
|
||||
ttl int64
|
||||
DryRun bool
|
||||
}
|
||||
|
||||
type gdEndpoint struct {
|
||||
endpoint *endpoint.Endpoint
|
||||
action int
|
||||
}
|
||||
|
||||
type gdRecordField struct {
|
||||
Data string `json:"data"`
|
||||
Name string `json:"name"`
|
||||
TTL int64 `json:"ttl"`
|
||||
Type string `json:"type"`
|
||||
Port *int `json:"port,omitempty"`
|
||||
Priority *int `json:"priority,omitempty"`
|
||||
Weight *int64 `json:"weight,omitempty"`
|
||||
Protocol *string `json:"protocol,omitempty"`
|
||||
Service *string `json:"service,omitempty"`
|
||||
}
|
||||
|
||||
type gdUpdateRecordField struct {
|
||||
Data string `json:"data"`
|
||||
Name string `json:"name"`
|
||||
TTL int64 `json:"ttl"`
|
||||
Port *int `json:"port,omitempty"`
|
||||
Priority *int `json:"priority,omitempty"`
|
||||
Weight *int64 `json:"weight,omitempty"`
|
||||
Protocol *string `json:"protocol,omitempty"`
|
||||
Service *string `json:"service,omitempty"`
|
||||
}
|
||||
|
||||
type gdRecords struct {
|
||||
records []gdRecordField
|
||||
changed bool
|
||||
zone string
|
||||
}
|
||||
|
||||
type gdZone struct {
|
||||
CreatedAt string
|
||||
Domain string
|
||||
DomainID int64
|
||||
ExpirationProtected bool
|
||||
Expires string
|
||||
ExposeWhois bool
|
||||
HoldRegistrar bool
|
||||
Locked bool
|
||||
NameServers *[]string
|
||||
Privacy bool
|
||||
RenewAuto bool
|
||||
RenewDeadline string
|
||||
Renewable bool
|
||||
Status string
|
||||
TransferProtected bool
|
||||
}
|
||||
|
||||
type gdZoneIDName map[string]*gdRecords
|
||||
|
||||
func (z gdZoneIDName) add(zoneID string, zoneRecord *gdRecords) {
|
||||
z[zoneID] = zoneRecord
|
||||
}
|
||||
|
||||
func (z gdZoneIDName) findZoneRecord(hostname string) (suitableZoneID string, suitableZoneRecord *gdRecords) {
|
||||
for zoneID, zoneRecord := range z {
|
||||
if hostname == zoneRecord.zone || strings.HasSuffix(hostname, "."+zoneRecord.zone) {
|
||||
if suitableZoneRecord == nil || len(zoneRecord.zone) > len(suitableZoneRecord.zone) {
|
||||
suitableZoneID = zoneID
|
||||
suitableZoneRecord = zoneRecord
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// NewGoDaddyProvider initializes a new GoDaddy DNS based Provider.
|
||||
func NewGoDaddyProvider(ctx context.Context, domainFilter endpoint.DomainFilter, ttl int64, apiKey, apiSecret string, useOTE, dryRun bool) (*GDProvider, error) {
|
||||
client, err := NewClient(useOTE, apiKey, apiSecret)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &GDProvider{
|
||||
client: client,
|
||||
domainFilter: domainFilter,
|
||||
ttl: maxOf(gdMinimalTTL, ttl),
|
||||
DryRun: dryRun,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *GDProvider) zones() ([]string, error) {
|
||||
zones := []gdZone{}
|
||||
filteredZones := []string{}
|
||||
|
||||
if err := p.client.Get("/v1/domains?statuses=ACTIVE", &zones); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, zone := range zones {
|
||||
if p.domainFilter.Match(zone.Domain) {
|
||||
filteredZones = append(filteredZones, zone.Domain)
|
||||
log.Debugf("GoDaddy: %s zone found", zone.Domain)
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("GoDaddy: %d zones found", len(filteredZones))
|
||||
|
||||
return filteredZones, nil
|
||||
}
|
||||
|
||||
func (p *GDProvider) zonesRecords(ctx context.Context, all bool) ([]string, []gdRecords, error) {
|
||||
var allRecords []gdRecords
|
||||
zones, err := p.zones()
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if len(zones) == 0 {
|
||||
allRecords = []gdRecords{}
|
||||
} else if len(zones) == 1 {
|
||||
record, err := p.records(&ctx, zones[0], all)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
allRecords = append(allRecords, *record)
|
||||
} else {
|
||||
chRecords := make(chan gdRecords, len(zones))
|
||||
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
|
||||
for _, zoneName := range zones {
|
||||
zone := zoneName
|
||||
eg.Go(func() error {
|
||||
record, err := p.records(&ctx, zone, all)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
chRecords <- *record
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if err := eg.Wait(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
close(chRecords)
|
||||
|
||||
for records := range chRecords {
|
||||
allRecords = append(allRecords, records)
|
||||
}
|
||||
}
|
||||
|
||||
return zones, allRecords, nil
|
||||
}
|
||||
|
||||
func (p *GDProvider) records(ctx *context.Context, zone string, all bool) (*gdRecords, error) {
|
||||
var recordsIds []gdRecordField
|
||||
|
||||
log.Debugf("GoDaddy: Getting records for %s", zone)
|
||||
|
||||
if err := p.client.Get(fmt.Sprintf("/v1/domains/%s/records", zone), &recordsIds); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if all {
|
||||
return &gdRecords{
|
||||
zone: zone,
|
||||
records: recordsIds,
|
||||
}, nil
|
||||
}
|
||||
|
||||
results := &gdRecords{
|
||||
zone: zone,
|
||||
records: make([]gdRecordField, 0, len(recordsIds)),
|
||||
}
|
||||
|
||||
for _, rec := range recordsIds {
|
||||
if provider.SupportedRecordType(rec.Type) {
|
||||
log.Debugf("GoDaddy: Record %s for %s is %+v", rec.Name, zone, rec)
|
||||
|
||||
results.records = append(results.records, rec)
|
||||
} else {
|
||||
log.Infof("GoDaddy: Discard record %s for %s is %+v", rec.Name, zone, rec)
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (p *GDProvider) groupByNameAndType(zoneRecords []gdRecords) []*endpoint.Endpoint {
|
||||
endpoints := []*endpoint.Endpoint{}
|
||||
|
||||
// group supported records by name and type
|
||||
groupsByZone := map[string]map[string][]gdRecordField{}
|
||||
|
||||
for _, zone := range zoneRecords {
|
||||
groups := map[string][]gdRecordField{}
|
||||
|
||||
groupsByZone[zone.zone] = groups
|
||||
|
||||
for _, r := range zone.records {
|
||||
groupBy := fmt.Sprintf("%s - %s", r.Type, r.Name)
|
||||
|
||||
if _, ok := groups[groupBy]; !ok {
|
||||
groups[groupBy] = []gdRecordField{}
|
||||
}
|
||||
|
||||
groups[groupBy] = append(groups[groupBy], r)
|
||||
}
|
||||
}
|
||||
|
||||
// create single endpoint with all the targets for each name/type
|
||||
for zoneName, groups := range groupsByZone {
|
||||
for _, records := range groups {
|
||||
targets := []string{}
|
||||
|
||||
for _, record := range records {
|
||||
targets = append(targets, record.Data)
|
||||
}
|
||||
|
||||
var recordName string
|
||||
|
||||
if records[0].Name == "@" {
|
||||
recordName = strings.TrimPrefix(zoneName, ".")
|
||||
} else {
|
||||
recordName = strings.TrimPrefix(fmt.Sprintf("%s.%s", records[0].Name, zoneName), ".")
|
||||
}
|
||||
|
||||
endpoint := endpoint.NewEndpointWithTTL(
|
||||
recordName,
|
||||
records[0].Type,
|
||||
endpoint.TTL(records[0].TTL),
|
||||
targets...,
|
||||
)
|
||||
|
||||
endpoints = append(endpoints, endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
return endpoints
|
||||
}
|
||||
|
||||
// Records returns the list of records in all relevant zones.
|
||||
func (p *GDProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
|
||||
_, records, err := p.zonesRecords(ctx, false)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
endpoints := p.groupByNameAndType(records)
|
||||
|
||||
log.Infof("GoDaddy: %d endpoints have been found", len(endpoints))
|
||||
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
func (p *GDProvider) appendChange(action int, endpoints []*endpoint.Endpoint, allChanges []gdEndpoint) []gdEndpoint {
|
||||
for _, e := range endpoints {
|
||||
allChanges = append(allChanges, gdEndpoint{
|
||||
action: action,
|
||||
endpoint: e,
|
||||
})
|
||||
}
|
||||
|
||||
return allChanges
|
||||
}
|
||||
|
||||
func (p *GDProvider) changeAllRecords(endpoints []gdEndpoint, zoneRecords []*gdRecords) error {
|
||||
zoneNameIDMapper := gdZoneIDName{}
|
||||
|
||||
for _, zoneRecord := range zoneRecords {
|
||||
zoneNameIDMapper.add(zoneRecord.zone, zoneRecord)
|
||||
}
|
||||
|
||||
for _, e := range endpoints {
|
||||
dnsName := e.endpoint.DNSName
|
||||
zone, zoneRecord := zoneNameIDMapper.findZoneRecord(dnsName)
|
||||
|
||||
if zone == "" {
|
||||
log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected", dnsName)
|
||||
} else {
|
||||
dnsName = strings.TrimSuffix(dnsName, "."+zone)
|
||||
|
||||
if e.endpoint.RecordType == endpoint.RecordTypeA && (len(dnsName) == 0) {
|
||||
dnsName = "@"
|
||||
}
|
||||
|
||||
for _, target := range e.endpoint.Targets {
|
||||
change := gdRecordField{
|
||||
Type: e.endpoint.RecordType,
|
||||
Name: dnsName,
|
||||
TTL: p.ttl,
|
||||
Data: target,
|
||||
}
|
||||
|
||||
if e.endpoint.RecordTTL.IsConfigured() {
|
||||
change.TTL = maxOf(gdMinimalTTL, int64(e.endpoint.RecordTTL))
|
||||
}
|
||||
|
||||
if err := zoneRecord.applyChange(e.action, p.client, change, p.DryRun); err != nil {
|
||||
log.Errorf("Unable to apply change %s on record %s, %v", actionNames[e.action], change, err)
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ApplyChanges applies a given set of changes in a given zone.
|
||||
func (p *GDProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
|
||||
if countTargets(changes) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, records, err := p.zonesRecords(ctx, true)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
changedZoneRecords := make([]*gdRecords, len(records))
|
||||
|
||||
for i := range records {
|
||||
changedZoneRecords[i] = &records[i]
|
||||
}
|
||||
|
||||
allChanges := make([]gdEndpoint, 0, countTargets(changes))
|
||||
|
||||
allChanges = p.appendChange(gdDelete, changes.Delete, allChanges)
|
||||
allChanges = p.appendChange(gdDelete, changes.UpdateOld, allChanges)
|
||||
allChanges = p.appendChange(gdCreate, changes.UpdateNew, allChanges)
|
||||
allChanges = p.appendChange(gdCreate, changes.Create, allChanges)
|
||||
|
||||
log.Infof("GoDaddy: %d changes will be done", len(allChanges))
|
||||
|
||||
if err = p.changeAllRecords(allChanges, changedZoneRecords); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *gdRecords) addRecord(client gdClient, change gdRecordField, dryRun bool) error {
|
||||
var response GDErrorResponse
|
||||
|
||||
log.Debugf("GoDaddy: Add an entry %s to zone %s", change.String(), p.zone)
|
||||
|
||||
p.records = append(p.records, change)
|
||||
p.changed = true
|
||||
|
||||
if dryRun {
|
||||
log.Infof("[DryRun] - Add record %s.%s of type %s %s", change.Name, p.zone, change.Type, toString(change))
|
||||
} else if err := client.Patch(fmt.Sprintf("/v1/domains/%s/records", p.zone), []gdRecordField{change}, &response); err != nil {
|
||||
log.Errorf("Add record %s.%s of type %s failed: %s", change.Name, p.zone, change.Type, response)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *gdRecords) updateRecord(client gdClient, change gdRecordField, dryRun bool) error {
|
||||
log.Debugf("GoDaddy: Update an entry %s to zone %s", change.String(), p.zone)
|
||||
|
||||
for index, record := range p.records {
|
||||
if record.Type == change.Type && record.Name == change.Name {
|
||||
var response GDErrorResponse
|
||||
|
||||
p.records[index] = change
|
||||
p.changed = true
|
||||
|
||||
changed := []gdUpdateRecordField{{
|
||||
Data: change.Data,
|
||||
Name: change.Name,
|
||||
TTL: change.TTL,
|
||||
Port: change.Port,
|
||||
Priority: change.Priority,
|
||||
Weight: change.Weight,
|
||||
Protocol: change.Protocol,
|
||||
Service: change.Service,
|
||||
}}
|
||||
|
||||
if dryRun {
|
||||
log.Infof("[DryRun] - Update record %s.%s of type %s %s", change.Name, p.zone, change.Type, toString(changed))
|
||||
} else if err := client.Patch(fmt.Sprintf("/v1/domains/%s/records/%s", p.zone, change.Type), changed, &response); err != nil {
|
||||
log.Errorf("Update record %s.%s of type %s failed: %v", change.Name, p.zone, change.Type, response)
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove one record from the record list
|
||||
func (p *gdRecords) deleteRecord(client gdClient, change gdRecordField, dryRun bool) error {
|
||||
log.Debugf("GoDaddy: Delete an entry %s to zone %s", change.String(), p.zone)
|
||||
|
||||
deleteIndex := -1
|
||||
|
||||
for index, record := range p.records {
|
||||
if record.Type == change.Type && record.Name == change.Name && record.Data == change.Data {
|
||||
deleteIndex = index
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if deleteIndex >= 0 {
|
||||
var response GDErrorResponse
|
||||
|
||||
p.records[deleteIndex] = p.records[len(p.records)-1]
|
||||
|
||||
p.records = p.records[:len(p.records)-1]
|
||||
p.changed = true
|
||||
|
||||
if dryRun {
|
||||
log.Infof("[DryRun] - Delete record %s.%s of type %s %s", change.Name, p.zone, change.Type, toString(change))
|
||||
} else if err := client.Delete(fmt.Sprintf("/v1/domains/%s/records/%s/%s", p.zone, change.Type, change.Name), &response); err != nil {
|
||||
log.Errorf("Delete record %s.%s of type %s failed: %v", change.Name, p.zone, change.Type, response)
|
||||
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
log.Warnf("GoDaddy: record in zone %s not found %s to delete", p.zone, change.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *gdRecords) applyChange(action int, client gdClient, change gdRecordField, dryRun bool) error {
|
||||
switch action {
|
||||
case gdCreate:
|
||||
return p.addRecord(client, change, dryRun)
|
||||
case gdUpdate:
|
||||
return p.updateRecord(client, change, dryRun)
|
||||
case gdDelete:
|
||||
return p.deleteRecord(client, change, dryRun)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c gdRecordField) String() string {
|
||||
return fmt.Sprintf("%s %d IN %s %s", c.Name, c.TTL, c.Type, c.Data)
|
||||
}
|
||||
|
||||
func countTargets(p *plan.Changes) int {
|
||||
changes := [][]*endpoint.Endpoint{p.Create, p.UpdateNew, p.UpdateOld, p.Delete}
|
||||
count := 0
|
||||
|
||||
for _, endpoints := range changes {
|
||||
for _, endpoint := range endpoints {
|
||||
count += len(endpoint.Targets)
|
||||
}
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
func maxOf(vars ...int64) int64 {
|
||||
max := vars[0]
|
||||
|
||||
for _, i := range vars {
|
||||
if max < i {
|
||||
max = i
|
||||
}
|
||||
}
|
||||
|
||||
return max
|
||||
}
|
||||
|
||||
func toString(obj interface{}) string {
|
||||
b, err := json.MarshalIndent(obj, "", " ")
|
||||
|
||||
if err != nil {
|
||||
return fmt.Sprintf("<%v>", err)
|
||||
}
|
||||
|
||||
return string(b)
|
||||
}
|
448
provider/godaddy/godaddy_test.go
Normal file
448
provider/godaddy/godaddy_test.go
Normal file
@ -0,0 +1,448 @@
|
||||
/*
|
||||
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 godaddy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
"sigs.k8s.io/external-dns/plan"
|
||||
)
|
||||
|
||||
type mockGoDaddyClient struct {
|
||||
mock.Mock
|
||||
currentTest *testing.T
|
||||
}
|
||||
|
||||
func newMockGoDaddyClient(t *testing.T) *mockGoDaddyClient {
|
||||
return &mockGoDaddyClient{
|
||||
currentTest: t,
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
zoneNameExampleOrg string = "example.org"
|
||||
zoneNameExampleNet string = "example.net"
|
||||
)
|
||||
|
||||
func (c *mockGoDaddyClient) Post(endpoint string, input interface{}, output interface{}) error {
|
||||
log.Infof("POST: %s - %v", endpoint, input)
|
||||
stub := c.Called(endpoint, input)
|
||||
data, _ := json.Marshal(stub.Get(0))
|
||||
json.Unmarshal(data, output)
|
||||
return stub.Error(1)
|
||||
}
|
||||
|
||||
func (c *mockGoDaddyClient) Patch(endpoint string, input interface{}, output interface{}) error {
|
||||
log.Infof("PATCH: %s - %v", endpoint, input)
|
||||
stub := c.Called(endpoint, input)
|
||||
data, _ := json.Marshal(stub.Get(0))
|
||||
json.Unmarshal(data, output)
|
||||
return stub.Error(1)
|
||||
}
|
||||
|
||||
func (c *mockGoDaddyClient) Put(endpoint string, input interface{}, output interface{}) error {
|
||||
log.Infof("PUT: %s - %v", endpoint, input)
|
||||
stub := c.Called(endpoint, input)
|
||||
data, _ := json.Marshal(stub.Get(0))
|
||||
json.Unmarshal(data, output)
|
||||
return stub.Error(1)
|
||||
}
|
||||
|
||||
func (c *mockGoDaddyClient) Get(endpoint string, output interface{}) error {
|
||||
log.Infof("GET: %s", endpoint)
|
||||
stub := c.Called(endpoint)
|
||||
data, _ := json.Marshal(stub.Get(0))
|
||||
json.Unmarshal(data, output)
|
||||
return stub.Error(1)
|
||||
}
|
||||
|
||||
func (c *mockGoDaddyClient) Delete(endpoint string, output interface{}) error {
|
||||
log.Infof("DELETE: %s", endpoint)
|
||||
stub := c.Called(endpoint)
|
||||
data, _ := json.Marshal(stub.Get(0))
|
||||
json.Unmarshal(data, output)
|
||||
return stub.Error(1)
|
||||
}
|
||||
|
||||
func TestGoDaddyZones(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
client := newMockGoDaddyClient(t)
|
||||
provider := &GDProvider{
|
||||
client: client,
|
||||
domainFilter: endpoint.NewDomainFilter([]string{"com"}),
|
||||
}
|
||||
|
||||
// Basic zones
|
||||
client.On("Get", "/v1/domains?statuses=ACTIVE").Return([]gdZone{
|
||||
{
|
||||
Domain: "example.com",
|
||||
},
|
||||
{
|
||||
Domain: "example.net",
|
||||
},
|
||||
}, nil).Once()
|
||||
|
||||
domains, err := provider.zones()
|
||||
|
||||
assert.NoError(err)
|
||||
assert.Contains(domains, "example.com")
|
||||
assert.NotContains(domains, "example.net")
|
||||
|
||||
client.AssertExpectations(t)
|
||||
|
||||
// Error on getting zones
|
||||
client.On("Get", "/v1/domains?statuses=ACTIVE").Return(nil, ErrAPIDown).Once()
|
||||
domains, err = provider.zones()
|
||||
assert.Error(err)
|
||||
assert.Nil(domains)
|
||||
client.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestGoDaddyZoneRecords(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
client := newMockGoDaddyClient(t)
|
||||
provider := &GDProvider{
|
||||
client: client,
|
||||
}
|
||||
|
||||
// Basic zones records
|
||||
client.On("Get", "/v1/domains?statuses=ACTIVE").Return([]gdZone{
|
||||
{
|
||||
Domain: zoneNameExampleNet,
|
||||
},
|
||||
}, nil).Once()
|
||||
|
||||
client.On("Get", "/v1/domains/example.net/records").Return([]gdRecordField{
|
||||
{
|
||||
Name: "godaddy",
|
||||
Type: "NS",
|
||||
TTL: gdMinimalTTL,
|
||||
Data: "203.0.113.42",
|
||||
},
|
||||
{
|
||||
Name: "godaddy",
|
||||
Type: "A",
|
||||
TTL: gdMinimalTTL,
|
||||
Data: "203.0.113.42",
|
||||
},
|
||||
}, nil).Once()
|
||||
|
||||
zones, records, err := provider.zonesRecords(context.TODO(), true)
|
||||
|
||||
assert.NoError(err)
|
||||
|
||||
assert.ElementsMatch(zones, []string{
|
||||
zoneNameExampleNet,
|
||||
})
|
||||
|
||||
assert.ElementsMatch(records, []gdRecords{
|
||||
{
|
||||
zone: zoneNameExampleNet,
|
||||
records: []gdRecordField{
|
||||
{
|
||||
Name: "godaddy",
|
||||
Type: "NS",
|
||||
TTL: gdMinimalTTL,
|
||||
Data: "203.0.113.42",
|
||||
},
|
||||
{
|
||||
Name: "godaddy",
|
||||
Type: "A",
|
||||
TTL: gdMinimalTTL,
|
||||
Data: "203.0.113.42",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client.AssertExpectations(t)
|
||||
|
||||
// Error on getting zones list
|
||||
client.On("Get", "/v1/domains?statuses=ACTIVE").Return(nil, ErrAPIDown).Once()
|
||||
zones, records, err = provider.zonesRecords(context.TODO(), false)
|
||||
assert.Error(err)
|
||||
assert.Nil(zones)
|
||||
assert.Nil(records)
|
||||
client.AssertExpectations(t)
|
||||
|
||||
// Error on getting zone records
|
||||
client.On("Get", "/v1/domains?statuses=ACTIVE").Return([]gdZone{
|
||||
{
|
||||
Domain: zoneNameExampleNet,
|
||||
},
|
||||
}, nil).Once()
|
||||
|
||||
client.On("Get", "/v1/domains/example.net/records").Return(nil, ErrAPIDown).Once()
|
||||
|
||||
zones, records, err = provider.zonesRecords(context.TODO(), false)
|
||||
|
||||
assert.Error(err)
|
||||
assert.Nil(zones)
|
||||
assert.Nil(records)
|
||||
client.AssertExpectations(t)
|
||||
|
||||
// Error on getting zone record detail
|
||||
client.On("Get", "/v1/domains?statuses=ACTIVE").Return([]gdZone{
|
||||
{
|
||||
Domain: zoneNameExampleNet,
|
||||
},
|
||||
}, nil).Once()
|
||||
|
||||
client.On("Get", "/v1/domains/example.net/records").Return(nil, ErrAPIDown).Once()
|
||||
|
||||
zones, records, err = provider.zonesRecords(context.TODO(), false)
|
||||
assert.Error(err)
|
||||
assert.Nil(zones)
|
||||
assert.Nil(records)
|
||||
client.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestGoDaddyRecords(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
client := newMockGoDaddyClient(t)
|
||||
provider := &GDProvider{
|
||||
client: client,
|
||||
}
|
||||
|
||||
// Basic zones records
|
||||
client.On("Get", "/v1/domains?statuses=ACTIVE").Return([]gdZone{
|
||||
{
|
||||
Domain: zoneNameExampleOrg,
|
||||
},
|
||||
{
|
||||
Domain: zoneNameExampleNet,
|
||||
},
|
||||
}, nil).Once()
|
||||
|
||||
client.On("Get", "/v1/domains/example.org/records").Return([]gdRecordField{
|
||||
{
|
||||
Name: "@",
|
||||
Type: "A",
|
||||
TTL: gdMinimalTTL,
|
||||
Data: "203.0.113.42",
|
||||
},
|
||||
{
|
||||
Name: "www",
|
||||
Type: "CNAME",
|
||||
TTL: gdMinimalTTL,
|
||||
Data: "example.org",
|
||||
},
|
||||
}, nil).Once()
|
||||
|
||||
client.On("Get", "/v1/domains/example.net/records").Return([]gdRecordField{
|
||||
{
|
||||
Name: "godaddy",
|
||||
Type: "A",
|
||||
TTL: gdMinimalTTL,
|
||||
Data: "203.0.113.42",
|
||||
},
|
||||
{
|
||||
Name: "godaddy",
|
||||
Type: "A",
|
||||
TTL: gdMinimalTTL,
|
||||
Data: "203.0.113.43",
|
||||
},
|
||||
}, nil).Once()
|
||||
|
||||
endpoints, err := provider.Records(context.TODO())
|
||||
assert.NoError(err)
|
||||
|
||||
// Little fix for multi targets endpoint
|
||||
for _, endpoint := range endpoints {
|
||||
sort.Strings(endpoint.Targets)
|
||||
}
|
||||
|
||||
assert.ElementsMatch(endpoints, []*endpoint.Endpoint{
|
||||
{
|
||||
DNSName: "godaddy.example.net",
|
||||
RecordType: "A",
|
||||
RecordTTL: gdMinimalTTL,
|
||||
Labels: endpoint.NewLabels(),
|
||||
Targets: []string{
|
||||
"203.0.113.42",
|
||||
"203.0.113.43",
|
||||
},
|
||||
},
|
||||
{
|
||||
DNSName: "example.org",
|
||||
RecordType: "A",
|
||||
RecordTTL: gdMinimalTTL,
|
||||
Labels: endpoint.NewLabels(),
|
||||
Targets: []string{
|
||||
"203.0.113.42",
|
||||
},
|
||||
},
|
||||
{
|
||||
DNSName: "www.example.org",
|
||||
RecordType: "CNAME",
|
||||
RecordTTL: gdMinimalTTL,
|
||||
Labels: endpoint.NewLabels(),
|
||||
Targets: []string{
|
||||
"example.org",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client.AssertExpectations(t)
|
||||
|
||||
// Error getting zone
|
||||
client.On("Get", "/v1/domains?statuses=ACTIVE").Return(nil, ErrAPIDown).Once()
|
||||
endpoints, err = provider.Records(context.TODO())
|
||||
assert.Error(err)
|
||||
assert.Nil(endpoints)
|
||||
client.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestGoDaddyChange(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
client := newMockGoDaddyClient(t)
|
||||
provider := &GDProvider{
|
||||
client: client,
|
||||
}
|
||||
|
||||
changes := plan.Changes{
|
||||
Create: []*endpoint.Endpoint{
|
||||
{
|
||||
DNSName: ".example.net",
|
||||
RecordType: "A",
|
||||
RecordTTL: gdMinimalTTL,
|
||||
Targets: []string{
|
||||
"203.0.113.42",
|
||||
},
|
||||
},
|
||||
},
|
||||
Delete: []*endpoint.Endpoint{
|
||||
{
|
||||
DNSName: "godaddy.example.net",
|
||||
RecordType: "A",
|
||||
Targets: []string{
|
||||
"203.0.113.43",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Fetch domains
|
||||
client.On("Get", "/v1/domains?statuses=ACTIVE").Return([]gdZone{
|
||||
{
|
||||
Domain: zoneNameExampleNet,
|
||||
},
|
||||
}, nil).Once()
|
||||
|
||||
// Fetch record
|
||||
client.On("Get", "/v1/domains/example.net/records").Return([]gdRecordField{
|
||||
{
|
||||
Name: "godaddy",
|
||||
Type: "A",
|
||||
TTL: gdMinimalTTL,
|
||||
Data: "203.0.113.43",
|
||||
},
|
||||
}, nil).Once()
|
||||
|
||||
// Add entry
|
||||
client.On("Patch", "/v1/domains/example.net/records", []gdRecordField{
|
||||
{
|
||||
Name: "@",
|
||||
Type: "A",
|
||||
TTL: gdMinimalTTL,
|
||||
Data: "203.0.113.42",
|
||||
},
|
||||
}).Return(nil, nil).Once()
|
||||
|
||||
// Delete entry
|
||||
client.On("Delete", "/v1/domains/example.net/records/A/godaddy").Return(nil, nil).Once()
|
||||
|
||||
assert.NoError(provider.ApplyChanges(context.TODO(), &changes))
|
||||
|
||||
client.AssertExpectations(t)
|
||||
}
|
||||
|
||||
const (
|
||||
operationFailedTestErrCode = "GD500"
|
||||
operationFailedTestReason = "Could not apply request"
|
||||
recordNotFoundErrCode = "GD404"
|
||||
recordNotFoundReason = "The requested record is not found in DNS zone"
|
||||
)
|
||||
|
||||
func TestGoDaddyErrorResponse(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
client := newMockGoDaddyClient(t)
|
||||
provider := &GDProvider{
|
||||
client: client,
|
||||
}
|
||||
|
||||
changes := plan.Changes{
|
||||
Create: []*endpoint.Endpoint{
|
||||
{
|
||||
DNSName: ".example.net",
|
||||
RecordType: "A",
|
||||
RecordTTL: gdMinimalTTL,
|
||||
Targets: []string{
|
||||
"203.0.113.42",
|
||||
},
|
||||
},
|
||||
},
|
||||
Delete: []*endpoint.Endpoint{
|
||||
{
|
||||
DNSName: "godaddy.example.net",
|
||||
RecordType: "A",
|
||||
Targets: []string{
|
||||
"203.0.113.43",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Fetch domains
|
||||
client.On("Get", "/v1/domains?statuses=ACTIVE").Return([]gdZone{
|
||||
{
|
||||
Domain: zoneNameExampleNet,
|
||||
},
|
||||
}, nil).Once()
|
||||
|
||||
// Fetch record
|
||||
client.On("Get", "/v1/domains/example.net/records").Return([]gdRecordField{
|
||||
{
|
||||
Name: "godaddy",
|
||||
Type: "A",
|
||||
TTL: gdMinimalTTL,
|
||||
Data: "203.0.113.43",
|
||||
},
|
||||
}, nil).Once()
|
||||
|
||||
// Delete entry
|
||||
client.On("Delete", "/v1/domains/example.net/records/A/godaddy").Return(GDErrorResponse{
|
||||
Code: operationFailedTestErrCode,
|
||||
Message: operationFailedTestReason,
|
||||
Fields: []GDErrorField{{
|
||||
Code: recordNotFoundErrCode,
|
||||
Message: recordNotFoundReason,
|
||||
}},
|
||||
}, errors.New(operationFailedTestReason)).Once()
|
||||
|
||||
assert.Error(provider.ApplyChanges(context.TODO(), &changes))
|
||||
|
||||
client.AssertExpectations(t)
|
||||
}
|
@ -152,14 +152,6 @@ func (p *HetznerProvider) submitChanges(ctx context.Context, changes []*HetznerC
|
||||
"zone_id": change.ZoneID,
|
||||
}).Info("Changing record")
|
||||
|
||||
change.ResourceRecordSet.Name = strings.TrimSuffix(change.ResourceRecordSet.Name, "."+change.ZoneName)
|
||||
if change.ResourceRecordSet.Name == change.ZoneName {
|
||||
change.ResourceRecordSet.Name = "@"
|
||||
}
|
||||
if change.ResourceRecordSet.RecordType == endpoint.RecordTypeCNAME {
|
||||
change.ResourceRecordSet.Value += "."
|
||||
}
|
||||
|
||||
switch change.Action {
|
||||
case hetznerCreate:
|
||||
record := hclouddns.HCloudRecord{
|
||||
|
@ -16,6 +16,7 @@ package hetzner
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/maxatome/go-testdeep/td"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
@ -45,12 +46,18 @@ type mockHCloudClientAdapter interface {
|
||||
UpdateRecordBulk(record []hclouddns.HCloudRecord) (hclouddns.HCloudAnswerUpdateRecords, error)
|
||||
}
|
||||
|
||||
type MockAction struct {
|
||||
Name string
|
||||
RecordData hclouddns.HCloudRecord
|
||||
}
|
||||
|
||||
type mockHCloudClient struct {
|
||||
Token string `yaml:"token"`
|
||||
Actions []MockAction
|
||||
}
|
||||
|
||||
// New instance
|
||||
func mockHCloudNew(t string) mockHCloudClientAdapter {
|
||||
func mockHCloudNew(t string) *mockHCloudClient {
|
||||
return &mockHCloudClient{
|
||||
Token: t,
|
||||
}
|
||||
@ -116,12 +123,26 @@ func (m *mockHCloudClient) GetRecords(params hclouddns.HCloudGetRecordsParams) (
|
||||
}, nil
|
||||
}
|
||||
func (m *mockHCloudClient) UpdateRecord(record hclouddns.HCloudRecord) (hclouddns.HCloudAnswerGetRecord, error) {
|
||||
m.Actions = append(m.Actions, MockAction{
|
||||
Name: "UpdateRecord",
|
||||
RecordData: record,
|
||||
})
|
||||
return hclouddns.HCloudAnswerGetRecord{}, nil
|
||||
}
|
||||
func (m *mockHCloudClient) DeleteRecord(ID string) (hclouddns.HCloudAnswerDeleteRecord, error) {
|
||||
m.Actions = append(m.Actions, MockAction{
|
||||
Name: "DeleteRecord",
|
||||
RecordData: hclouddns.HCloudRecord{
|
||||
ID: ID,
|
||||
},
|
||||
})
|
||||
return hclouddns.HCloudAnswerDeleteRecord{}, nil
|
||||
}
|
||||
func (m *mockHCloudClient) CreateRecord(record hclouddns.HCloudRecord) (hclouddns.HCloudAnswerGetRecord, error) {
|
||||
m.Actions = append(m.Actions, MockAction{
|
||||
Name: "CreateRecord",
|
||||
RecordData: record,
|
||||
})
|
||||
return hclouddns.HCloudAnswerGetRecord{}, nil
|
||||
}
|
||||
func (m *mockHCloudClient) CreateRecordBulk(record []hclouddns.HCloudRecord) (hclouddns.HCloudAnswerCreateRecords, error) {
|
||||
@ -220,14 +241,65 @@ func TestHetznerProvider_ApplyChanges(t *testing.T) {
|
||||
}
|
||||
|
||||
changes.Create = []*endpoint.Endpoint{
|
||||
{DNSName: "test.org", Targets: endpoint.Targets{"target"}},
|
||||
{DNSName: "test.test.org", Targets: endpoint.Targets{"target"}, RecordTTL: 666},
|
||||
{DNSName: "blindage.org", Targets: endpoint.Targets{"target"}},
|
||||
{DNSName: "test.blindage.org", Targets: endpoint.Targets{"target"}, RecordTTL: 666},
|
||||
}
|
||||
changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "test.test.org", Targets: endpoint.Targets{"target-new"}, RecordType: "A", RecordTTL: 777}}
|
||||
changes.Delete = []*endpoint.Endpoint{{DNSName: "test.test.org", Targets: endpoint.Targets{"target"}, RecordType: "A"}}
|
||||
changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "test.blindage.org", Targets: endpoint.Targets{"target-new"}, RecordType: "A", RecordTTL: 777}}
|
||||
changes.Delete = []*endpoint.Endpoint{{DNSName: "test.blindage.org", Targets: endpoint.Targets{"target"}, RecordType: "A"}}
|
||||
|
||||
err := mockedProvider.ApplyChanges(context.Background(), changes)
|
||||
if err != nil {
|
||||
t.Errorf("should not fail, %s", err)
|
||||
}
|
||||
|
||||
if len(mockedClient.Actions) != 4 {
|
||||
t.Errorf("should be 4 changes not %d", len(mockedClient.Actions))
|
||||
}
|
||||
}
|
||||
|
||||
func TestHetznerProvider_ApplyChangesCreateUpdateCname(t *testing.T) {
|
||||
changes := &plan.Changes{}
|
||||
mockedClient := mockHCloudNew("myHetznerToken")
|
||||
mockedProvider := &HetznerProvider{
|
||||
Client: mockedClient,
|
||||
}
|
||||
|
||||
changes.Create = []*endpoint.Endpoint{
|
||||
{DNSName: "test-cname.blindage.org", Targets: endpoint.Targets{"target"}, RecordTTL: 666, RecordType: "CNAME"},
|
||||
}
|
||||
changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "test-cname2.blindage.org", Targets: endpoint.Targets{"target-new"}, RecordType: "CNAME", RecordTTL: 777}}
|
||||
|
||||
err := mockedProvider.ApplyChanges(context.Background(), changes)
|
||||
if err != nil {
|
||||
t.Errorf("should not fail, %s", err)
|
||||
}
|
||||
|
||||
td.Cmp(t, mockedClient.Actions, []MockAction{
|
||||
{
|
||||
Name: "CreateRecord",
|
||||
RecordData: hclouddns.HCloudRecord{
|
||||
RecordType: "CNAME",
|
||||
ID: "",
|
||||
Created: "",
|
||||
Modified: "",
|
||||
ZoneID: "HetznerZoneID",
|
||||
Name: "test-cname",
|
||||
Value: "target.",
|
||||
TTL: 666,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "UpdateRecord",
|
||||
RecordData: hclouddns.HCloudRecord{
|
||||
RecordType: "CNAME",
|
||||
ID: "",
|
||||
Created: "",
|
||||
Modified: "",
|
||||
ZoneID: "HetznerZoneID",
|
||||
Name: "test-cname2",
|
||||
Value: "target-new.",
|
||||
TTL: 777,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -46,6 +46,7 @@ type InfobloxConfig struct {
|
||||
DryRun bool
|
||||
View string
|
||||
MaxResults int
|
||||
FQDNRexEx string
|
||||
}
|
||||
|
||||
// InfobloxProvider implements the DNS provider for Infoblox.
|
||||
@ -56,6 +57,7 @@ type InfobloxProvider struct {
|
||||
zoneIDFilter provider.ZoneIDFilter
|
||||
view string
|
||||
dryRun bool
|
||||
fqdnRegEx string
|
||||
}
|
||||
|
||||
type infobloxRecordSet struct {
|
||||
@ -63,28 +65,36 @@ type infobloxRecordSet struct {
|
||||
res interface{}
|
||||
}
|
||||
|
||||
// MaxResultsRequestBuilder implements a HttpRequestBuilder which sets the
|
||||
// _max_results query parameter on all get requests
|
||||
type MaxResultsRequestBuilder struct {
|
||||
// ExtendedRequestBuilder implements a HttpRequestBuilder which sets
|
||||
// additional query parameter on all get requests
|
||||
type ExtendedRequestBuilder struct {
|
||||
fqdnRegEx string
|
||||
maxResults int
|
||||
ibclient.WapiRequestBuilder
|
||||
}
|
||||
|
||||
// NewMaxResultsRequestBuilder returns a MaxResultsRequestBuilder which adds
|
||||
// NewExtendedRequestBuilder returns a ExtendedRequestBuilder which adds
|
||||
// _max_results query parameter to all GET requests
|
||||
func NewMaxResultsRequestBuilder(maxResults int) *MaxResultsRequestBuilder {
|
||||
return &MaxResultsRequestBuilder{
|
||||
func NewExtendedRequestBuilder(maxResults int, fqdnRegEx string) *ExtendedRequestBuilder {
|
||||
return &ExtendedRequestBuilder{
|
||||
fqdnRegEx: fqdnRegEx,
|
||||
maxResults: maxResults,
|
||||
}
|
||||
}
|
||||
|
||||
// BuildRequest prepares the api request. it uses BuildRequest of
|
||||
// WapiRequestBuilder and then add the _max_requests parameter
|
||||
func (mrb *MaxResultsRequestBuilder) BuildRequest(t ibclient.RequestType, obj ibclient.IBObject, ref string, queryParams ibclient.QueryParams) (req *http.Request, err error) {
|
||||
func (mrb *ExtendedRequestBuilder) BuildRequest(t ibclient.RequestType, obj ibclient.IBObject, ref string, queryParams ibclient.QueryParams) (req *http.Request, err error) {
|
||||
req, err = mrb.WapiRequestBuilder.BuildRequest(t, obj, ref, queryParams)
|
||||
if req.Method == "GET" {
|
||||
query := req.URL.Query()
|
||||
if mrb.maxResults > 0 {
|
||||
query.Set("_max_results", strconv.Itoa(mrb.maxResults))
|
||||
}
|
||||
_, ok := obj.(*ibclient.ZoneAuth)
|
||||
if ok && t == ibclient.GET && mrb.fqdnRegEx != "" {
|
||||
query.Set("fqdn~", mrb.fqdnRegEx)
|
||||
}
|
||||
req.URL.RawQuery = query.Encode()
|
||||
}
|
||||
return
|
||||
@ -110,9 +120,9 @@ func NewInfobloxProvider(infobloxConfig InfobloxConfig) (*InfobloxProvider, erro
|
||||
)
|
||||
|
||||
var requestBuilder ibclient.HttpRequestBuilder
|
||||
if infobloxConfig.MaxResults != 0 {
|
||||
if infobloxConfig.MaxResults != 0 || infobloxConfig.FQDNRexEx != "" {
|
||||
// use our own HttpRequestBuilder which sets _max_results parameter on GET requests
|
||||
requestBuilder = NewMaxResultsRequestBuilder(infobloxConfig.MaxResults)
|
||||
requestBuilder = NewExtendedRequestBuilder(infobloxConfig.MaxResults, infobloxConfig.FQDNRexEx)
|
||||
} else {
|
||||
// use the default HttpRequestBuilder of the infoblox client
|
||||
requestBuilder = &ibclient.WapiRequestBuilder{}
|
||||
@ -132,6 +142,7 @@ func NewInfobloxProvider(infobloxConfig InfobloxConfig) (*InfobloxProvider, erro
|
||||
zoneIDFilter: infobloxConfig.ZoneIDFilter,
|
||||
dryRun: infobloxConfig.DryRun,
|
||||
view: infobloxConfig.View,
|
||||
fqdnRegEx: infobloxConfig.FQDNRexEx,
|
||||
}
|
||||
|
||||
return provider, nil
|
||||
|
@ -533,7 +533,7 @@ func TestInfobloxZones(t *testing.T) {
|
||||
assert.Equal(t, provider.findZone(zones, "lvl2-2.lvl1-2.example.com").Fqdn, "example.com")
|
||||
}
|
||||
|
||||
func TestMaxResultsRequestBuilder(t *testing.T) {
|
||||
func TestExtendedRequestFDQDRegExBuilder(t *testing.T) {
|
||||
hostConfig := ibclient.HostConfig{
|
||||
Host: "localhost",
|
||||
Port: "8080",
|
||||
@ -542,7 +542,29 @@ func TestMaxResultsRequestBuilder(t *testing.T) {
|
||||
Version: "2.3.1",
|
||||
}
|
||||
|
||||
requestBuilder := NewMaxResultsRequestBuilder(54321)
|
||||
requestBuilder := NewExtendedRequestBuilder(0, "^staging.*test.com$")
|
||||
requestBuilder.Init(hostConfig)
|
||||
|
||||
obj := ibclient.NewZoneAuth(ibclient.ZoneAuth{})
|
||||
|
||||
req, _ := requestBuilder.BuildRequest(ibclient.GET, obj, "", ibclient.QueryParams{})
|
||||
|
||||
assert.True(t, req.URL.Query().Get("fqdn~") == "^staging.*test.com$")
|
||||
|
||||
req, _ = requestBuilder.BuildRequest(ibclient.CREATE, obj, "", ibclient.QueryParams{})
|
||||
|
||||
assert.True(t, req.URL.Query().Get("fqdn~") == "")
|
||||
}
|
||||
func TestExtendedRequestMaxResultsBuilder(t *testing.T) {
|
||||
hostConfig := ibclient.HostConfig{
|
||||
Host: "localhost",
|
||||
Port: "8080",
|
||||
Username: "user",
|
||||
Password: "abcd",
|
||||
Version: "2.3.1",
|
||||
}
|
||||
|
||||
requestBuilder := NewExtendedRequestBuilder(54321, "")
|
||||
requestBuilder.Init(hostConfig)
|
||||
|
||||
obj := ibclient.NewRecordCNAME(ibclient.RecordCNAME{Zone: "foo.bar.com"})
|
||||
|
@ -166,7 +166,7 @@ func (c *PDNSAPIClient) ListZones() (zones []pgo.Zone, resp *http.Response, err
|
||||
func (c *PDNSAPIClient) PartitionZones(zones []pgo.Zone) (filteredZones []pgo.Zone, residualZones []pgo.Zone) {
|
||||
if c.domainFilter.IsConfigured() {
|
||||
for _, zone := range zones {
|
||||
if c.domainFilter.Match(zone.Name) {
|
||||
if c.domainFilter.Match(zone.Name) || c.domainFilter.MatchParent(zone.Name) {
|
||||
filteredZones = append(filteredZones, zone)
|
||||
} else {
|
||||
residualZones = append(residualZones, zone)
|
||||
@ -257,13 +257,16 @@ func NewPDNSProvider(ctx context.Context, config PDNSConfig) (*PDNSProvider, err
|
||||
|
||||
func (p *PDNSProvider) convertRRSetToEndpoints(rr pgo.RrSet) (endpoints []*endpoint.Endpoint, _ error) {
|
||||
endpoints = []*endpoint.Endpoint{}
|
||||
var targets = []string{}
|
||||
|
||||
for _, record := range rr.Records {
|
||||
// If a record is "Disabled", it's not supposed to be "visible"
|
||||
if !record.Disabled {
|
||||
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(rr.Name, rr.Type_, endpoint.TTL(rr.Ttl), record.Content))
|
||||
targets = append(targets, record.Content)
|
||||
}
|
||||
}
|
||||
|
||||
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(rr.Name, rr.Type_, endpoint.TTL(rr.Ttl), targets...))
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user