Merge branch 'master' into oci-auth-instance-principal

This commit is contained in:
Eric R. Rath 2021-07-26 09:49:41 -07:00
commit ce2ccebb2e
139 changed files with 43192 additions and 1767 deletions

15
.dockerignore Normal file
View 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
View File

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

View File

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

@ -48,3 +48,6 @@ external-dns
vendor/
profile.cov
# github codespaces
.venv/

View File

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

View File

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

View File

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

View File

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

@ -5,6 +5,11 @@ approvers:
- raffo
- njuettner
reviewers:
- njuettner
- raffo
- seanmalloy
emeritus_approvers:
- hjacobs
- linki

View File

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

View File

@ -1,5 +1,5 @@
# See https://cloud.google.com/cloud-build/docs/build-config
timeout: 3000s
timeout: 5000s
options:
substitution_option: ALLOW_LOOSE
steps:

View File

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

View File

@ -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": "",
},
},
},
},
},
)
}

View File

@ -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: []

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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
View 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
```

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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{""},

View File

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

View File

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

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

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

View File

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

View File

@ -1,4 +1,4 @@
apiVersion: rbac.authorization.k8s.io/v1beta1
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: external-dns

View File

@ -1,4 +1,4 @@
apiVersion: rbac.authorization.k8s.io/v1beta1
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: external-dns-viewer

View File

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

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

View File

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

View File

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

View File

@ -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 == "" {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

@ -0,0 +1,6 @@
approvers:
- seanmalloy
- vinny-sabatini
reviewers:
- seanmalloy
- vinny-sabatini

1044
provider/bluecat/bluecat.go Normal file

File diff suppressed because it is too large Load Diff

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

View File

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

View 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

File diff suppressed because it is too large Load Diff

120
provider/gandi/client.go Normal file
View 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
View 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
}

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

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

View File

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

View File

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

View File

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

View File

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

View File

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