git: merge upstream master

Signed-off-by: Enrique Gonzalez <goga.enrique@gmail.com>
This commit is contained in:
Enrique Gonzalez 2021-03-03 12:05:11 +01:00
commit fd365034da
No known key found for this signature in database
GPG Key ID: 77F8AF07F85DEBB9
112 changed files with 4895 additions and 1859 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

View File

@ -1,569 +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
## 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
- Add regex domain filters (#1504) @offzale
## 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

@ -19,8 +19,12 @@ 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

View File

@ -16,13 +16,17 @@ FROM golang:1.15 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

@ -45,8 +45,11 @@ ExternalDNS' current release is `v0.7`. This version allows you to keep selected
* [NS1](https://ns1.com/)
* [TransIP](https://www.transip.eu/domain-name/)
* [VinylDNS](https://www.vinyldns.io)
* [Vultr](https://www.vultr.com)
* [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)
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.
@ -77,6 +80,7 @@ The following table clarifies the current status of the providers according to t
| Google Cloud DNS | Stable | |
| AWS Route 53 | Stable | |
| AWS Cloud Map | Beta | |
| Akamai Edge DNS | Beta | |
| AzureDNS | Beta | |
| CloudFlare | Beta | |
| RcodeZero | Alpha | |
@ -96,11 +100,11 @@ The following table clarifies the current status of the providers according to t
| TransIP | Alpha | |
| VinylDNS | Alpha | |
| RancherDNS | Alpha | |
| Akamai FastDNS | Alpha | |
| OVH | Alpha | |
| Scaleway DNS | Alpha | @Sh4d1 |
| Vultr | Alpha | |
| UltraDNS | Alpha | |
| GoDaddy | Alpha | |
## Running ExternalDNS:
@ -153,6 +157,7 @@ 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)
### Running Locally
@ -164,8 +169,8 @@ from source.
Next, run an application and expose it via a Kubernetes Service:
```console
$ kubectl run nginx --image=nginx --replicas=1 --port=80
$ kubectl expose deployment 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.
@ -182,6 +187,14 @@ $ kubectl annotate service nginx "external-dns.alpha.kubernetes.io/ttl=10"
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."
```
If the service is not of type Loadbalancer you need the --publish-internal-services flag.
Locally run a single sync loop of ExternalDNS.
```console

View File

@ -117,6 +117,8 @@ type Controller struct {
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
}
// RunOnce runs a single iteration of a reconciliation loop.
@ -139,12 +141,15 @@ func (c *Controller) RunOnce(ctx context.Context) error {
}
sourceEndpointsTotal.Set(float64(len(endpoints)))
endpoints = c.Registry.AdjustEndpoints(endpoints)
plan := &plan.Plan{
Policies: []plan.Policy{c.Policy},
Current: records,
Desired: endpoints,
DomainFilter: c.DomainFilter,
PropertyComparator: c.Registry.PropertyValuesEqual,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
plan = plan.Calculate()
@ -163,7 +168,7 @@ func (c *Controller) RunOnce(ctx context.Context) error {
// MinInterval is used as window for batching events
const MinInterval = 5 * time.Second
// RunOnceThrottled makes sure execution happens at most once per interval.
// ScheduleRunOnce makes sure execution happens at most once per interval.
func (c *Controller) ScheduleRunOnce(now time.Time) {
c.nextRunAtMux.Lock()
defer c.nextRunAtMux.Unlock()

View File

@ -17,7 +17,7 @@
## Summary
[ExternalDNS](https://github.com/kubernetes-sigs/external-dns) is a project that synchronizes Kubernetes Services, Ingresses and other Kubernetes resources to DNS backends for several DNS providers.
[ExternalDNS](https://github.com/kubernetes-sigs/external-dns) is a project that synchronizes Kubernetes' Services, Ingresses and other Kubernetes resources to DNS backends for several DNS providers.
The projects was started as a Kubernetes Incubator project in February 2017 and being the Kubernetes incubation initiative officially over, the maintainers want to propose the project to be moved to the kubernetes GitHub organization or to kubernetes-sigs, under the sponsorship of sig-network.
@ -33,7 +33,7 @@ When the project was proposed (see the [original discussion](https://github.com/
* Route53-kubernetes - [https://github.com/wearemolecule/route53-kubernetes](https://github.com/wearemolecule/route53-kubernetes)
ExternalDNS goal from the beginning was to provide an officially supported solution to those problems.
ExternalDNS' goal from the beginning was to provide an officially supported solution to those problems.
After two years of development, the project is still in the kubernetes-sigs.
@ -74,7 +74,7 @@ Moving the ExternalDNS project outside of Kubernetes projects would cause:
* Problems (re-)establishing user trust which could eventually lead to fragmentation and duplication.
* It would be hard to establish in which organization the project should be moved to. The most natural would be Zalandos organization, being the company that put most of the work on the project. While it is possible to assume Zalandos commitment to open-source, that would be a strategic mistake for the project community and for the Kubernetes ecosystem due to the obvious lack of neutrality.
* It would be hard to establish in which organization the project should be moved to. The most natural would be Zalando's organization, being the company that put most of the work on the project. While it is possible to assume Zalando's commitment to open-source, that would be a strategic mistake for the project community and for the Kubernetes ecosystem due to the obvious lack of neutrality.
* Lack of resources to test, lack of issue management via automation.
@ -91,7 +91,7 @@ We have evidence that many companies are using ExternalDNS in production, but it
The project was quoted by a number of tutorials on the web, including the [official tutorials from AWS](https://aws.amazon.com/blogs/opensource/unified-service-discovery-ecs-kubernetes/).
ExternalDNS cant be consider to be "done": while the core functionality has been implemented, there is lack of integration testing and structural changes that are needed.
ExternalDNS can't be consider to be "done": while the core functionality has been implemented, there is lack of integration testing and structural changes that are needed.
Those are identified in the project roadmap, which is roughly made of the following items:
@ -129,7 +129,7 @@ The high number of providers contributed to the project pose a maintainability c
The project uses the free quota of TravisCI to run tests for the project.
The release pipeline for the project is currently fully owned by Zalando. It runs on the internal system of the company (closed source) which external maintainers/users cant access and that pushes images to the publicly accessible docker registry available at the URL `registry.opensource.zalan.do`.
The release pipeline for the project is currently fully owned by Zalando. It runs on the internal system of the company (closed source) which external maintainers/users can't access and that pushes images to the publicly accessible docker registry available at the URL `registry.opensource.zalan.do`.
The docker registry service is provided as best effort with no sort of SLA and the maintainers team openly suggests the users to build and maintain their own docker image based on the provided Dockerfiles.
@ -149,8 +149,8 @@ The following are risks that were identified:
We think that the following actions will constitute appropriate mitigations:
* Decoupling the providers via an API will allow us to resolve the problem of the providers. Being the project already more than 2 years old and given that there are 18 providers implemented, we possess enough informations to define an API that we can be stable in a short timeframe. Once this is stable, the problem of testing the providers can be deferred to be a providers responsibility. This will also reduce the scope of External DNS core code, which means that there will be no need for a further increase of the maintaining team.
* Decoupling the providers via an API will allow us to resolve the problem of the providers. Being the project already more than 2 years old and given that there are 18 providers implemented, we possess enough information to define an API that we can be stable in a short timeframe. Once this is stable, the problem of testing the providers can be deferred to be a provider's responsibility. This will also reduce the scope of External DNS core code, which means that there will be no need for a further increase of the maintaining team.
* We added integration testing for the main cloud providers to the roadmap for the 1.0 release to make sure that we cover the mostly used ones. We believe that this item should be tackled independently from the decoupling of providers as it would be capable of generating value independently from the result of the decoupling efforts.
* With the move to the Kubernetes incubation, we hope that we will be able to access the testing resources of the Kubernetes project. In this way, we hope to decouple the project from the dependency on Zalandos internal CI tool. This will help open up the possibility to increase the visibility on the project from external contributors, which currently would be blocked by the lack of access to the software used for the whole release pipeline.
* With the move to the Kubernetes incubation, we hope that we will be able to access the testing resources of the Kubernetes project. In this way, we hope to decouple the project from the dependency on Zalando's internal CI tool. This will help open up the possibility to increase the visibility on the project from external contributors, which currently would be blocked by the lack of access to the software used for the whole release pipeline.

View File

@ -8,7 +8,7 @@
- [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl)
Compile and run locally against a remote k8s cluster.
```
```shell
git clone https://github.com/kubernetes-sigs/external-dns.git && cd external-dns
make build
# login to remote k8s cluster
@ -16,14 +16,14 @@ make build
```
Run linting, unit tests, and coverage report.
```
```shell
make lint
make test
make cover-html
```
Build container image.
```
```shell
make build.docker
```
@ -31,7 +31,7 @@ make build.docker
ExternalDNS's sources of DNS records live in package [source](../../source). They implement the `Source` interface that has a single method `Endpoints` which returns the represented source's objects converted to `Endpoints`. Endpoints are just a tuple of DNS name and target where target can be an IP or another hostname.
For example, the `ServiceSource` returns all Services converted to `Endpoints` where the hostname is the value of the `external-dns.alpha.kubernetes.io/hostname` annotation and the target is the IP of the load balancer.
For example, the `ServiceSource` returns all Services converted to `Endpoints` where the hostname is the value of the `external-dns.alpha.kubernetes.io/hostname` annotation and the target is the IP of the load balancer or where the hostname is the value of the `external-dns.alpha.kubernetes.io/internal-hostname` annotation and the target is the IP of the service CLusterIP.
This list of endpoints is passed to the [Plan](../../plan) which determines the difference between the current DNS records and the desired list of `Endpoints`.
@ -49,10 +49,10 @@ A typical way to start on, e.g. a CoreDNS provider, would be to add a `coredns.g
Note, how your provider doesn't need to know anything about where the DNS records come from, nor does it have to figure out the difference between the current and the desired state, it merely executes the actions calculated by the plan.
# Running Github Actions locally
# Running GitHub Actions locally
You can also extend the CI workflow which is currently implemented as Github Action within the [workflow](../../.github/workflow) folder.
In order to test your changes before commiting you can leverage [act](https://github.com/nektos/act) to run the Github Action locally.
You can also extend the CI workflow which is currently implemented as GitHub Action within the [workflow](https://github.com/kubernetes-sigs/external-dns/tree/HEAD/.github/workflows) folder.
In order to test your changes before committing you can leverage [act](https://github.com/nektos/act) to run the GitHub Action locally.
Follow the installation instructions in the nektos/act [README.md](https://github.com/nektos/act/blob/master/README.md).
Afterwards just run `act` within the root folder of the project.

View File

@ -25,7 +25,7 @@ All sources live in package `source`.
* `ContourIngressRouteSource`: collects all Contour IngressRoutes and returns them as Endpoint objects. The desired DNS name corresponds to the `virtualhost.fqdn` listed within the spec of each IngressRoute object.
* `FakeSource`: returns a random list of Endpoints for the purpose of testing providers without having access to a Kubernetes cluster.
* `ConnectorSource`: returns a list of Endpoint objects which are served by a tcp server configured through `connector-source-server` flag.
* `CRDSource`: returns a list of Endpoint objects sourced from the spec of CRD objects. For more details refer to [CRD source](../crd-source.md) documentation.
* `CRDSource`: returns a list of Endpoint objects sourced from the spec of CRD objects. For more details refer to [CRD source](crd-source.md) documentation.
* `EmptySource`: returns an empty list of Endpoint objects for the purpose of testing and cleaning out entries.
### Providers

View File

@ -57,7 +57,7 @@ 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 corresponding value.
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.
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 +75,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 +194,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 +216,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: ...
```
@ -302,7 +304,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

@ -2,7 +2,7 @@
## Background
[Project proposal](https://groups.google.com/forum/#!searchin/kubernetes-dev/external$20dns%7Csort:relevance/kubernetes-dev/2wGQUB0fUuE/9OXz01i2BgAJ)
[Project proposal](https://groups.google.com/forum/#!searching/kubernetes-dev/external$20dns%7Csort:relevance/kubernetes-dev/2wGQUB0fUuE/9OXz01i2BgAJ)
[Initial discussion](https://docs.google.com/document/d/1ML_q3OppUtQKXan6Q42xIq2jelSoIivuXI8zExbc6ec/edit#heading=h.1pgkuagjhm4p)

View File

@ -0,0 +1,251 @@
# Setting up External-DNS for Services on Akamai Edge DNS
## Prerequisites
Akamai Edge DNS (formally known as Fast DNS) provider support was first released in External-DNS v0.5.18
### 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)
### Akamai Edge DNS Authentication
The Akamai Edge DNS provider requires valid Akamai Edgegrid API authentication credentials to access zones and manage associated 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:
| Edgegrid Auth Key | External-DNS Cmd Line Key | Environment/ConfigMap Key | Description |
| ----------------- | ------------------------- | ------------------------- | ----------- |
| host | akamai-serviceconsumerdomain | EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN | Akamai Edgegrid API server |
| access_token | akamai-access-token | EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN | Akamai Edgegrid API access token |
| 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.
| 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.
## 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.
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:
### 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.6
args:
- --source=service # or ingress or both
- --provider=akamai
- --domain-filter=example.com
# zone-id-filter may be specified as well to filter on contract ID
- --registry=txt
- --txt-owner-id={{ owner-id-for-this-external-dns }}
env:
- name: EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN
valueFrom:
secretKeyRef:
name: external-dns
key: EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN
- name: EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN
valueFrom:
secretKeyRef:
name: external-dns
key: EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN
- name: EXTERNAL_DNS_AKAMAI_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: external-dns
key: EXTERNAL_DNS_AKAMAI_CLIENT_SECRET
- name: EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN
valueFrom:
secretKeyRef:
name: external-dns
key: EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN
```
### 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","endpoints","pods"]
verbs: ["get","watch","list"]
- apiGroups: ["extensions","networking.k8s.io"]
resources: ["ingresses"]
verbs: ["get","watch","list"]
- apiGroups: [""]
resources: ["nodes"]
verbs: ["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:
containers:
- name: external-dns
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
args:
- --source=service # or ingress or both
- --provider=akamai
- --domain-filter=example.com
# zone-id-filter may be specified as well to filter on contract ID
- --registry=txt
- --txt-owner-id={{ owner-id-for-this-external-dns }}
env:
- name: EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN
valueFrom:
secretKeyRef:
name: external-dns
key: EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN
- name: EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN
valueFrom:
secretKeyRef:
name: external-dns
key: EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN
- name: EXTERNAL_DNS_AKAMAI_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: external-dns
key: EXTERNAL_DNS_AKAMAI_CLIENT_SECRET
- name: EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN
valueFrom:
secretKeyRef:
name: external-dns
key: EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN
```
Create the deployment for External-DNS:
```
$ kubectl create -f externaldns.yaml
```
## 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: nginx.example.com
external-dns.alpha.kubernetes.io/ttl: "600" #optional
spec:
selector:
app: nginx
type: LoadBalancer
ports:
- protocol: TCP
port: 80
targetPort: 80
```
Create the deployment, service and ingress object:
```
$ kubectl create -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.
The records can be validated 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:
```
$ kubectl delete -f nginx.yaml
$ 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.

View File

@ -1,192 +0,0 @@
# Setting up Akamai FastDNS
## Prerequisites
Akamai FastDNS provider support was added via [this PR](https://github.com/kubernetes-sigs/external-dns/pull/1384), thus you need to use a release where this pr is included. This should be at least v0.5.18
The Akamai FastDNS provider expects that your zones, you wish to add records to, already exists
and are configured correctly. It does not add, remove or configure new zones in anyway.
To do this please refer to the [FastDNS documentation](https://learn.akamai.com/en-us/products/web_performance/fast_dns.html).
Additional data you will have to provide:
* Service Consumer Domain
* Access token
* Client token
* Client Secret
Make these available to external DNS somehow. In the following example a secret is used by referencing the secret and its keys in the env section of the deployment.
If you happen to have questions regarding authentication, please refer to the [API Client Authentication documentation](https://developer.akamai.com/legacy/introduction/Client_Auth.html)
## Deployment
Deploying external DNS for Akamai is actually nearly identical to deploying
it for other providers. This is what a sample `deployment.yaml` looks like:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: external-dns
labels:
app.kubernetes.io/name: external-dns
app.kubernetes.io/version: v0.6.0
spec:
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: external-dns
template:
metadata:
labels:
app.kubernetes.io/name: external-dns
app.kubernetes.io/version: v0.6.0
spec:
# Only use if you're also using RBAC
# serviceAccountName: external-dns
containers:
- name: external-dns
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
args:
- --source=ingress # or service or both
- --provider=akamai
- --registry=txt
- --txt-owner-id={{ owner-id-for-this-external-dns }}
env:
- name: EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN
valueFrom:
secretKeyRef:
name: external-dns
key: EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN
- name: EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN
valueFrom:
secretKeyRef:
name: external-dns
key: EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN
- name: EXTERNAL_DNS_AKAMAI_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: external-dns
key: EXTERNAL_DNS_AKAMAI_CLIENT_SECRET
- name: EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN
valueFrom:
secretKeyRef:
name: external-dns
key: EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN
```
## RBAC
If your cluster is RBAC enabled, you also need to setup the following, before you can run external-dns:
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-dns
namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: external-dns
rules:
- apiGroups: [""]
resources: ["services","endpoints","pods"]
verbs: ["get","watch","list"]
- apiGroups: ["extensions","networking.k8s.io"]
resources: ["ingresses"]
verbs: ["get","watch","list"]
- apiGroups: [""]
resources: ["nodes"]
verbs: ["list"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: external-dns-viewer
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: external-dns
subjects:
- kind: ServiceAccount
name: external-dns
namespace: default
```
## Verify ExternalDNS works (Ingress example)
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
kind: Ingress
metadata:
name: foo
annotations:
kubernetes.io/ingress.class: "nginx" # use the one that corresponds to your ingress controller.
spec:
rules:
- host: foo.bar.com
http:
paths:
- backend:
serviceName: foo
servicePort: 80
```
## Verify ExternalDNS works (Service example)
Create the following sample application to test that ExternalDNS works.
> For services ExternalDNS will look for the annotation `external-dns.alpha.kubernetes.io/hostname` on the service and use the corresponding value.
> If you want to give multiple names to service, you can set it to external-dns.alpha.kubernetes.io/hostname with a comma separator.
```yaml
apiVersion: v1
kind: Service
metadata:
name: nginx
annotations:
external-dns.alpha.kubernetes.io/hostname: nginx.external-dns-test.my-org.com
spec:
type: LoadBalancer
ports:
- port: 80
name: http
targetPort: 80
selector:
app: nginx
---
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
name: http
```
**Important!**: Don't run dig, nslookup or similar immediately. You'll get hit by [negative DNS caching](https://tools.ietf.org/html/rfc2308), which is hard to flush.
Wait about 30s-1m (interval for external-dns to kick in)

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

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
@ -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
@ -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
@ -410,10 +410,43 @@ For any given DNS name, only **one** of the following routing policies can be us
## Associating DNS records with healthchecks
You can configure Route53 to associate DNS records with healthchecks for automated DNS failover using
`external-dns.alpha.kubernetes.io/health-check-id: <health-check-id>` annotation.
`external-dns.alpha.kubernetes.io/aws-health-check-id: <health-check-id>` annotation.
Note: ExternalDNS does not support creating healthchecks, and assumes that `<health-check-id>` already exists.
## Govcloud caveats
Due to the special nature with how Route53 runs in Govcloud, there are a few tweaks in the deployment settings.
* An Environment variable with name of AWS_REGION set to either us-gov-west-1 or us-gov-east-1 is required. Otherwise it tries to lookup a region that does not exist in Govcloud and it errors out.
```yaml
env:
- name: AWS_REGION
value: us-gov-west-1
```
* Route53 in Govcloud does not allow aliases. Therefore, container args must be set so that it uses CNAMES and a txt-prefix must be set to something. Otherwise, it will try to create a TXT record with the same value than the CNAME itself, which is not allowed.
```yaml
args:
- --aws-prefer-cname
- --txt-prefix={{ YOUR_PREFIX }}
```
* The first two changes are needed if you use Route53 in Govcloud, which only supports private zones. There are also no cross account IAM whatsoever between Govcloud and commerical AWS accounts. If services and ingresses need to make Route 53 entries to an public zone in a commerical account, you will have set env variables of AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY with a key and secret to the commerical account that has the sufficient rights.
```yaml
env:
- name: AWS_ACCESS_KEY_ID
value: XXXXXXXXX
- name: AWS_SECRET_ACCESS_KEY
valueFrom:
secretKeyRef:
name: {{ YOUR_SECRET_NAME }}
key: {{ YOUR_SECRET_KEY }}
```
## Clean up
Make sure to delete all Service objects before terminating the cluster so all load balancers get cleaned up correctly.

View File

@ -1,12 +1,12 @@
# Set up ExternalDNS for Azure Private DNS
This tutorial describes how to set up ExternalDNS for managing records in Azure Private DNS.
This tutorial describes how to set up ExternalDNS for managing records in Azure Private DNS.
It comprises of the following steps:
1) Install NGINX Ingress Controller
2) Provision Azure Private DNS
3) Configure service principal for managing the zone
4) Deploy ExternalDNS
4) Deploy ExternalDNS
Everything will be deployed on Kubernetes.
Therefore, please see the subsequent prerequisites.
@ -26,25 +26,27 @@ $ helm install stable/nginx-ingress \
--name nginx-ingress \
--set controller.publishService.enabled=true
```
The parameter `controller.publishService.enabled` needs to be set to `true.`
The parameter `controller.publishService.enabled` needs to be set to `true.`
It will make the ingress controller update the endpoint records of ingress-resources to contain the external-ip of the loadbalancer serving the ingress-controller.
This is crucial as ExternalDNS reads those endpoints records when creating DNS-Records from ingress-resources.
In the subsequent parameter we will make use of this. If you don't want to work with ingress-resources in your later use, you can leave the parameter out.
Verify the correct propagation of the loadbalancer's ip by listing the ingresses.
```
$ kubectl get ingress
```
The address column should contain the ip for each ingress. ExternalDNS will pick up exactly this piece of information.
```
NAME HOSTS ADDRESS PORTS AGE
nginx1 sample1.aks.com 52.167.195.110 80 6d22h
nginx2 sample2.aks.com 52.167.195.110 80 6d21h
```
If you do not want to deploy the ingress controller with Helm, ensure to pass the following cmdline-flags to it through the mechanism of your choice:
```
@ -144,6 +146,8 @@ This is per default done through the file `~/.kube/config`.
For general background information on this see [kubernetes-docs](https://kubernetes.io/docs/tasks/access-application-cluster/access-cluster/).
Azure-CLI features functionality for automatically maintaining this file for AKS-Clusters. See [Azure-Docs](https://docs.microsoft.com/de-de/cli/azure/aks?view=azure-cli-latest#az-aks-get-credentials).
Follow the steps for [azure-dns provider](./azure.md#creating-configuration-file) to create a configuration file.
Then apply one of the following manifests depending on whether you use RBAC or not.
The credentials of the service principal are provided to ExternalDNS as environment-variables.
@ -167,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.7.6
args:
- --source=service
- --source=ingress
@ -175,13 +179,14 @@ spec:
- --provider=azure-private-dns
- --azure-resource-group=externaldns
- --azure-subscription-id=<use the id of your subscription>
env:
- name: AZURE_TENANT_ID
value: "<use the tenantId discovered during creation of service principal>"
- name: AZURE_CLIENT_ID
value: "<use the aadClientId discovered during creation of service principal>"
- name: AZURE_CLIENT_SECRET
value: "<use the aadClientSecret discovered during creation of service principal>"
volumeMounts:
- name: azure-config-file
mountPath: /etc/kubernetes
readOnly: true
volumes:
- name: azure-config-file
secret:
secretName: azure-config-file
```
### Manifest (for clusters with RBAC enabled, cluster access)
@ -200,7 +205,7 @@ rules:
resources: ["services","endpoints","pods"]
verbs: ["get","watch","list"]
- apiGroups: ["extensions","networking.k8s.io"]
resources: ["ingresses"]
resources: ["ingresses"]
verbs: ["get","watch","list"]
- apiGroups: [""]
resources: ["nodes"]
@ -237,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.7.6
args:
- --source=service
- --source=ingress
@ -245,13 +250,14 @@ spec:
- --provider=azure-private-dns
- --azure-resource-group=externaldns
- --azure-subscription-id=<use the id of your subscription>
env:
- name: AZURE_TENANT_ID
value: "<use the tenantId discovered during creation of service principal>"
- name: AZURE_CLIENT_ID
value: "<use the aadClientId discovered during creation of service principal>"
- name: AZURE_CLIENT_SECRET
value: "<use the aadClientSecret discovered during creation of service principal>"
volumeMounts:
- name: azure-config-file
mountPath: /etc/kubernetes
readOnly: true
volumes:
- name: azure-config-file
secret:
secretName: azure-config-file
```
### Manifest (for clusters with RBAC enabled, namespace access)
@ -307,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.7.6
args:
- --source=service
- --source=ingress
@ -315,13 +321,14 @@ spec:
- --provider=azure-private-dns
- --azure-resource-group=externaldns
- --azure-subscription-id=<use the id of your subscription>
env:
- name: AZURE_TENANT_ID
value: "<use the tenantId discovered during creation of service principal>"
- name: AZURE_CLIENT_ID
value: "<use the aadClientId discovered during creation of service principal>"
- name: AZURE_CLIENT_SECRET
value: "<use the aadClientSecret discovered during creation of service principal>"
volumeMounts:
- name: azure-config-file
mountPath: /etc/kubernetes
readOnly: true
volumes:
- name: azure-config-file
secret:
secretName: azure-config-file
```
Create the deployment for ExternalDNS:

View File

@ -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.7.6
args:
- --source=service
- --source=ingress
@ -261,7 +261,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
@ -331,7 +331,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

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

@ -1,5 +1,5 @@
# Setting up ExternalDNS for CoreDNS with minikube
This tutorial describes how to setup ExternalDNS for usage within a [minikube](https://github.com/kubernetes/minikube) cluster that makes use of [CoreDNS](https://github.com/coredns/coredns) and [nginx ingress controller](https://github.com/kubernetes/ingress-nginx).
This tutorial describes how to setup ExternalDNS for usage within a [minikube](https://github.com/kubernetes/minikube) cluster that makes use of [CoreDNS](https://github.com/coredns/coredns) and [nginx ingress controller](https://github.com/kubernetes/ingress-nginx).
You need to:
* install CoreDNS with [etcd](https://github.com/etcd-io/etcd) enabled
* install external-dns with coredns as a provider
@ -24,7 +24,7 @@ helm install stable/etcd-operator --name my-etcd-op
```
etcd cluster is installed with example yaml from etcd operator website.
```
kubectl apply -f https://raw.githubusercontent.com/coreos/etcd-operator/HEAD/example/example-etcd-cluster.yaml
kubectl apply -f https://raw.githubusercontent.com/coreos/etcd-operator/HEAD/example/example-etcd-cluster.yaml
```
### Installing CoreDNS
@ -34,7 +34,7 @@ wget https://raw.githubusercontent.com/helm/charts/HEAD/stable/coredns/values.ya
```
You need to edit/patch the file with below diff
```
```diff
diff --git a/values.yaml b/values.yaml
index 964e72b..e2fa934 100644
--- a/values.yaml
@ -68,7 +68,7 @@ index 964e72b..e2fa934 100644
# Complete example with all the options:
# - zones: # the `zones` block can be left out entirely, defaults to "."
```
**Note**:
**Note**:
* IP address of etcd's endpoint should be get from etcd client service. It should be "example-etcd-cluster-client" in this example. This IP address is used through this document for etcd endpoint configuration.
```
$ kubectl get svc example-etcd-cluster-client
@ -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
@ -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
@ -228,5 +228,5 @@ $ kubectl run -it --rm --restart=Never --image=infoblox/dnstools:latest dnstools
If you don't see a command prompt, try pressing enter.
dnstools# dig @10.100.4.143 nginx.example.org +short
10.0.2.15
dnstools#
dnstools#
```

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

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

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

View File

@ -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
@ -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.7.6
name: external-dns
securityContext:
fsGroup: 65534

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/v1beta1
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/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:
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.
@ -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
@ -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
@ -188,7 +188,7 @@ kafka-1.example.org
kafka-2.example.org
```
If you set `--fqdn-template={{name}}.example.org` you can ommit the annotation.
If you set `--fqdn-template={{name}}.example.org` you can omit the annotation.
Generally it is a better approach to use `--fqdn-template={{name}}.example.org`, because then
you would get the service name inside the generated A records:

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

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

View File

@ -78,7 +78,7 @@ rules:
See also current RBAC yaml files:
- [kube-ingress-aws-controller](https://github.com/zalando-incubator/kubernetes-on-aws/blob/dev/cluster/manifests/ingress-controller/01-rbac.yaml)
- [skipper](https://github.com/zalando-incubator/kubernetes-on-aws/blob/dev/cluster/manifests/skipper/rbac.yaml)
- [external-dns](https://github.com/zalando-incubator/kubernetes-on-aws/blob/dev/cluster/manifests/external-dns/rbac.yaml)
- [external-dns](https://github.com/zalando-incubator/kubernetes-on-aws/blob/dev/cluster/manifests/external-dns/01-rbac.yaml)
[3]: https://opensource.zalando.com/skipper/kubernetes/routegroups/#routegroups
[4]: https://opensource.zalando.com/skipper
@ -269,7 +269,7 @@ status:
```
ExternalDNS will create a A-records `echoserver.example.org`, that
use AWS ALIAS record to automatically maintain IP adresses of the NLB.
use AWS ALIAS record to automatically maintain IP addresses of the NLB.
## RouteGroup (optional)

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

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

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

@ -93,7 +93,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.
@ -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
@ -155,7 +155,7 @@ $ kubectl get services echo
$ kubectl get endpoints echo
```
Make sure everything looks correct, i.e the service is defined and recieves a
Make sure everything looks correct, i.e the service is defined and receives a
public IP, and that the endpoint also has a pod IP.
Once that's done, wait about 30s-1m (interval for external-dns to kick in), then do:

View File

@ -243,7 +243,7 @@ spec:
- --txt-owner-id=external-dns
- --annotation-filter=kubernetes.io/ingress.class=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
```
@ -281,7 +281,7 @@ spec:
- --txt-owner-id=dev.k8s.nexus
- --annotation-filter=kubernetes.io/ingress.class=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
```

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

View File

@ -166,6 +166,7 @@ rules:
- services
- endpoints
- pods
- nodes
verbs:
- get
- watch
@ -217,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
@ -257,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
@ -272,29 +277,111 @@ 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
### `external-dns` configuration
#### `external-dns` configuration
You'll want to configure `external-dns` similarly to the following:
```text
...
- --provider=rfc2136
- --rfc2136-host=123.123.123.123
- --rfc2136-host=192.168.0.1
- --rfc2136-port=53
- --rfc2136-zone=your-domain.com
- --rfc2136-zone=k8s.example.org
- --rfc2136-insecure
- --rfc2136-tsig-axfr # needed to enable zone transfers, which is required for deletion of records.
...
```
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 from all servers
#### 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 = YOURDOMAIN.COM
[realms]
YOURDOMAIN.COM = {
kdc = dc1.yourdomain.com
admin_server = dc1.yourdomain.com
}
[domain_realm]
yourdomain.com = YOURDOMAIN.COM
.yourdomain.com = 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=123.123.123.123
- --rfc2136-port=53
- --rfc2136-zone=your-domain.com
- --rfc2136-kerberos-username=your-domain-account
- --rfc2136-kerberos-password=your-domain-password
- --rfc2136-tsig-axfr # needed to enable zone transfers, which is required for deletion of records.
...
```

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
@ -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
@ -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
@ -263,7 +263,7 @@ $ kubectl create -f external-dns.yaml
```
- Depending on where you run your service from, it can take a few minutes for your cloud provider to create an external IP for the service.
- Please verify on the [UltraDNS UI](https://portal.ultradns.neustar) that the records have been created under the zone "example.com".
- Finally, you will need to clean up the deployment and service. Please verify on the UI afterwards that the records have been deleted from the zone example.com:
- Finally, you will need to clean up the deployment and service. Please verify on the UI afterwards that the records have been deleted from the zone "example.com":
```console
$ kubectl delete -f apple-banana-echo.yaml
$ kubectl delete -f expose-apple-banana-app.yaml

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

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

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 {

20
go.mod
View File

@ -8,17 +8,18 @@ require (
github.com/Azure/azure-sdk-for-go v45.1.0+incompatible
github.com/Azure/go-autorest/autorest v0.11.10
github.com/Azure/go-autorest/autorest/adal v0.9.5
github.com/Azure/go-autorest/autorest/azure/auth v0.5.3
github.com/Azure/go-autorest/autorest/to v0.4.0
github.com/akamai/AkamaiOPEN-edgegrid-golang v0.9.11
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.0.0
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 // indirect
github.com/alecthomas/colour v0.1.0 // indirect
github.com/alecthomas/kingpin v2.2.5+incompatible
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
github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba
github.com/digitalocean/godo v1.36.0
github.com/dnsimple/dnsimple-go v0.60.0
@ -33,7 +34,7 @@ require (
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
@ -44,9 +45,10 @@ require (
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/stretchr/testify v1.5.1
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/ultradns/ultradns-sdk-go v0.0.0-20200616202852-e62052662f60
@ -54,16 +56,20 @@ require (
github.com/vultr/govultr v0.4.2
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-20200324143707-d3edc9973b7e
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.2.8
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
k8s.io/api v0.18.8
k8s.io/apimachinery v0.18.8
k8s.io/client-go v0.18.8
k8s.io/kubernetes v1.13.0
)
replace (

538
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,7 @@ kind: Kustomization
images:
- name: k8s.gcr.io/external-dns/external-dns
newTag: v0.7.4
newTag: v0.7.6
resources:
- ./external-dns-deployment.yaml

27
main.go
View File

@ -46,6 +46,7 @@ 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/godaddy"
"sigs.k8s.io/external-dns/provider/google"
"sigs.k8s.io/external-dns/provider/hetzner"
"sigs.k8s.io/external-dns/provider/infoblox"
@ -157,7 +158,7 @@ func main() {
var p provider.Provider
switch cfg.Provider {
case "akamai":
p = akamai.NewAkamaiProvider(
p, err = akamai.NewAkamaiProvider(
akamai.AkamaiConfig{
DomainFilter: domainFilter,
ZoneIDFilter: zoneIDFilter,
@ -165,9 +166,10 @@ func main() {
ClientToken: cfg.AkamaiClientToken,
ClientSecret: cfg.AkamaiClientSecret,
AccessToken: cfg.AkamaiAccessToken,
EdgercPath: cfg.AkamaiEdgercPath,
EdgercSection: cfg.AkamaiEdgercSection,
DryRun: cfg.DryRun,
},
)
}, nil)
case "alibabacloud":
p, err = alibabacloud.NewAlibabaCloudProvider(cfg.AlibabaCloudConfigFile, domainFilter, zoneIDFilter, cfg.AlibabaCloudZoneType, cfg.DryRun)
case "aws":
@ -197,7 +199,7 @@ func main() {
case "azure-dns", "azure":
p, err = azure.NewAzureProvider(cfg.AzureConfigFile, domainFilter, zoneNameFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.DryRun)
case "azure-private-dns":
p, err = azure.NewAzurePrivateDNSProvider(domainFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.AzureSubscriptionID, cfg.DryRun)
p, err = azure.NewAzurePrivateDNSProvider(cfg.AzureConfigFile, domainFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.DryRun)
case "vinyldns":
p, err = vinyldns.NewVinylDNSProvider(domainFilter, zoneIDFilter, cfg.DryRun)
case "vultr":
@ -287,7 +289,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, nil)
case "ns1":
p, err = ns1.NewNS1Provider(
ns1.NS1Config{
@ -303,6 +305,8 @@ 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)
default:
log.Fatalf("unknown dns provider: %s", cfg.Provider)
}
@ -315,7 +319,7 @@ func main() {
case "noop":
r, err = registry.NewNoopRegistry(p)
case "txt":
r, err = registry.NewTXTRegistry(p, cfg.TXTPrefix, cfg.TXTSuffix, cfg.TXTOwnerID, cfg.TXTCacheInterval)
r, err = registry.NewTXTRegistry(p, cfg.TXTPrefix, cfg.TXTSuffix, cfg.TXTOwnerID, cfg.TXTCacheInterval, cfg.TXTWildcardReplacement)
case "aws-sd":
r, err = registry.NewAWSSDRegistry(p.(*awssd.AWSSDProvider), cfg.TXTOwnerID)
default:
@ -332,11 +336,12 @@ func main() {
}
ctrl := controller.Controller{
Source: endpointsSource,
Registry: r,
Policy: policy,
Interval: cfg.Interval,
DomainFilter: domainFilter,
Source: endpointsSource,
Registry: r,
Policy: policy,
Interval: cfg.Interval,
DomainFilter: domainFilter,
ManagedRecordTypes: cfg.ManagedDNSRecordTypes,
}
if cfg.Once {

View File

@ -23,6 +23,8 @@ import (
"strconv"
"time"
"sigs.k8s.io/external-dns/endpoint"
"github.com/alecthomas/kingpin"
"github.com/sirupsen/logrus"
@ -91,6 +93,8 @@ type Config struct {
AkamaiClientToken string
AkamaiClientSecret string
AkamaiAccessToken string
AkamaiEdgercPath string
AkamaiEdgercSection string
InfobloxGridHost string
InfobloxWapiPort int
InfobloxWapiUsername string
@ -126,6 +130,7 @@ type Config struct {
MetricsAddress string
LogLevel string
TXTCacheInterval time.Duration
TXTWildcardReplacement string
ExoscaleEndpoint string
ExoscaleAPIKey string `secure:"yes"`
ExoscaleAPISecret string `secure:"yes"`
@ -139,6 +144,9 @@ type Config struct {
RFC2136Port int
RFC2136Zone string
RFC2136Insecure bool
RFC2136GSSTSIG bool
RFC2136KerberosUsername string
RFC2136KerberosPassword string
RFC2136TSIGKeyName string
RFC2136TSIGSecret string `secure:"yes"`
RFC2136TSIGSecretAlg string
@ -150,6 +158,11 @@ type Config struct {
TransIPAccountName string
TransIPPrivateKeyFile string
DigitalOceanAPIPageSize int
ManagedDNSRecordTypes []string
GoDaddyAPIKey string `secure:"yes"`
GoDaddySecretKey string `secure:"yes"`
GoDaddyTTL int64
GoDaddyOTE bool
}
var defaultConfig = &Config{
@ -199,6 +212,8 @@ var defaultConfig = &Config{
AkamaiClientToken: "",
AkamaiClientSecret: "",
AkamaiAccessToken: "",
AkamaiEdgercSection: "",
AkamaiEdgercPath: "",
InfobloxGridHost: "",
InfobloxWapiPort: 443,
InfobloxWapiUsername: "admin",
@ -223,6 +238,7 @@ var defaultConfig = &Config{
TXTPrefix: "",
TXTSuffix: "",
TXTCacheInterval: 0,
TXTWildcardReplacement: "",
Interval: time.Minute,
Once: false,
DryRun: false,
@ -243,6 +259,9 @@ var defaultConfig = &Config{
RFC2136Port: 0,
RFC2136Zone: "",
RFC2136Insecure: false,
RFC2136GSSTSIG: false,
RFC2136KerberosUsername: "",
RFC2136KerberosPassword: "",
RFC2136TSIGKeyName: "",
RFC2136TSIGSecret: "",
RFC2136TSIGSecretAlg: "",
@ -253,6 +272,11 @@ var defaultConfig = &Config{
TransIPAccountName: "",
TransIPPrivateKeyFile: "",
DigitalOceanAPIPageSize: 50,
ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
GoDaddyAPIKey: "",
GoDaddySecretKey: "",
GoDaddyTTL: 600,
GoDaddyOTE: false,
}
// NewConfig returns new Config object
@ -313,7 +337,7 @@ func (cfg *Config) ParseFlags(args []string) error {
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)").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")
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("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)
@ -330,9 +354,10 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("crd-source-apiversion", "API version of the CRD for crd source, e.g. `externaldns.k8s.io/v1alpha1`, valid only when using crd source").Default(defaultConfig.CRDSourceAPIVersion).StringVar(&cfg.CRDSourceAPIVersion)
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)
// 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, cloudflare, rcodezero, digitalocean, hetzner, dnsimple, akamai, infoblox, dyn, designate, coredns, skydns, inmemory, ovh, pdns, oci, exoscale, linode, rfc2136, ns1, transip, vinyldns, rdns, scaleway, vultr, ultradns)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "aws-sd", "google", "azure", "azure-dns", "hetzner", "azure-private-dns", "alibabacloud", "cloudflare", "rcodezero", "digitalocean", "dnsimple", "akamai", "infoblox", "dyn", "designate", "coredns", "skydns", "inmemory", "ovh", "pdns", "oci", "exoscale", "linode", "rfc2136", "ns1", "transip", "vinyldns", "rdns", "scaleway", "vultr", "ultradns", "godaddy")
app.Flag("domain-filter", "Limit possible target zones by a domain suffix; specify multiple times for multiple domains (optional)").Default("").StringsVar(&cfg.DomainFilter)
app.Flag("exclude-domains", "Exclude subdomains (optional)").Default("").StringsVar(&cfg.ExcludeDomains)
app.Flag("regex-domain-filter", "Limit possible domains and target zones by a Regex filter; Overrides domain-filter (optional)").Default(defaultConfig.RegexDomainFilter.String()).RegexpVar(&cfg.RegexDomainFilter)
@ -360,10 +385,12 @@ func (cfg *Config) ParseFlags(args []string) error {
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)
app.Flag("akamai-serviceconsumerdomain", "When using the Akamai provider, specify the base URL (required when --provider=akamai)").Default(defaultConfig.AkamaiServiceConsumerDomain).StringVar(&cfg.AkamaiServiceConsumerDomain)
app.Flag("akamai-client-token", "When using the Akamai provider, specify the client token (required when --provider=akamai)").Default(defaultConfig.AkamaiClientToken).StringVar(&cfg.AkamaiClientToken)
app.Flag("akamai-client-secret", "When using the Akamai provider, specify the client secret (required when --provider=akamai)").Default(defaultConfig.AkamaiClientSecret).StringVar(&cfg.AkamaiClientSecret)
app.Flag("akamai-access-token", "When using the Akamai provider, specify the access token (required when --provider=akamai)").Default(defaultConfig.AkamaiAccessToken).StringVar(&cfg.AkamaiAccessToken)
app.Flag("akamai-serviceconsumerdomain", "When using the Akamai provider, specify the base URL (required when --provider=akamai and edgerc-path not specified)").Default(defaultConfig.AkamaiServiceConsumerDomain).StringVar(&cfg.AkamaiServiceConsumerDomain)
app.Flag("akamai-client-token", "When using the Akamai provider, specify the client token (required when --provider=akamai and edgerc-path not specified)").Default(defaultConfig.AkamaiClientToken).StringVar(&cfg.AkamaiClientToken)
app.Flag("akamai-client-secret", "When using the Akamai provider, specify the client secret (required when --provider=akamai and edgerc-path not specified)").Default(defaultConfig.AkamaiClientSecret).StringVar(&cfg.AkamaiClientSecret)
app.Flag("akamai-access-token", "When using the Akamai provider, specify the access token (required when --provider=akamai and edgerc-path not specified)").Default(defaultConfig.AkamaiAccessToken).StringVar(&cfg.AkamaiAccessToken)
app.Flag("akamai-edgerc-path", "When using the Akamai provider, specify the .edgerc file path. Path must be reachable form invocation environment. (required when --provider=akamai and *-token, secret serviceconsumerdomain not specified)").Default(defaultConfig.AkamaiEdgercPath).StringVar(&cfg.AkamaiEdgercPath)
app.Flag("akamai-edgerc-section", "When using the Akamai provider, specify the .edgerc file path (Optional when edgerc-path is specified)").Default(defaultConfig.AkamaiEdgercSection).StringVar(&cfg.AkamaiEdgercSection)
app.Flag("infoblox-grid-host", "When using the Infoblox provider, specify the Grid Manager host (required when --provider=infoblox)").Default(defaultConfig.InfobloxGridHost).StringVar(&cfg.InfobloxGridHost)
app.Flag("infoblox-wapi-port", "When using the Infoblox provider, specify the WAPI port (default: 443)").Default(strconv.Itoa(defaultConfig.InfobloxWapiPort)).IntVar(&cfg.InfobloxWapiPort)
app.Flag("infoblox-wapi-username", "When using the Infoblox provider, specify the WAPI username (default: admin)").Default(defaultConfig.InfobloxWapiUsername).StringVar(&cfg.InfobloxWapiUsername)
@ -374,7 +401,7 @@ func (cfg *Config) ParseFlags(args []string) error {
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("dyn-customer-name", "When using the Dyn provider, specify the Customer Name").Default("").StringVar(&cfg.DynCustomerName)
app.Flag("dyn-username", "When using the Dyn provider, specify the Username").Default("").StringVar(&cfg.DynUsername)
app.Flag("dyn-password", "When using the Dyn provider, specify the pasword").Default("").StringVar(&cfg.DynPassword)
app.Flag("dyn-password", "When using the Dyn provider, specify the password").Default("").StringVar(&cfg.DynPassword)
app.Flag("dyn-min-ttl", "Minimal TTL (in seconds) for records. This value will be used if the provided TTL for a service/ingress is lower than this.").IntVar(&cfg.DynMinTTLSeconds)
app.Flag("oci-config-file", "When using the OCI provider, specify the OCI configuration file (required when --provider=oci").Default(defaultConfig.OCIConfigFile).StringVar(&cfg.OCIConfigFile)
app.Flag("rcodezero-txt-encrypt", "When using the Rcodezero provider with txt registry option, set if TXT rrs are encrypted (default: false)").Default(strconv.FormatBool(defaultConfig.RcodezeroTXTEncrypt)).BoolVar(&cfg.RcodezeroTXTEncrypt)
@ -388,6 +415,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)
@ -408,6 +440,9 @@ 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-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)
// 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)
@ -421,6 +456,7 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("txt-owner-id", "When using the TXT registry, a name that identifies this instance of ExternalDNS (default: default)").Default(defaultConfig.TXTOwnerID).StringVar(&cfg.TXTOwnerID)
app.Flag("txt-prefix", "When using the TXT registry, a custom string that's prefixed to each ownership DNS record (optional). Mutual exclusive with txt-suffix!").Default(defaultConfig.TXTPrefix).StringVar(&cfg.TXTPrefix)
app.Flag("txt-suffix", "When using the TXT registry, a custom string that's suffixed to the host portion of each ownership DNS record (optional). Mutual exclusive with txt-prefix!").Default(defaultConfig.TXTSuffix).StringVar(&cfg.TXTSuffix)
app.Flag("txt-wildcard-replacement", "When using the TXT registry, a custom string that's used instead of an asterisk for TXT records corresponding to wildcard DNS records (optional)").Default(defaultConfig.TXTWildcardReplacement).StringVar(&cfg.TXTWildcardReplacement)
// 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)

View File

@ -23,6 +23,8 @@ import (
"testing"
"time"
"sigs.k8s.io/external-dns/endpoint"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -69,6 +71,8 @@ var (
AkamaiClientToken: "",
AkamaiClientSecret: "",
AkamaiAccessToken: "",
AkamaiEdgercPath: "",
AkamaiEdgercSection: "",
InfobloxGridHost: "",
InfobloxWapiPort: 443,
InfobloxWapiUsername: "admin",
@ -105,6 +109,7 @@ var (
TransIPAccountName: "",
TransIPPrivateKeyFile: "",
DigitalOceanAPIPageSize: 50,
ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
overriddenConfig = &Config{
@ -149,6 +154,8 @@ var (
AkamaiClientToken: "o184671d5307a388180fbf7f11dbdf46",
AkamaiClientSecret: "o184671d5307a388180fbf7f11dbdf46",
AkamaiAccessToken: "o184671d5307a388180fbf7f11dbdf46",
AkamaiEdgercPath: "/home/test/.edgerc",
AkamaiEdgercSection: "default",
InfobloxGridHost: "127.0.0.1",
InfobloxWapiPort: 8443,
InfobloxWapiUsername: "infoblox",
@ -191,6 +198,7 @@ var (
TransIPAccountName: "transip",
TransIPPrivateKeyFile: "/path/to/transip.key",
DigitalOceanAPIPageSize: 100,
ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
)
@ -240,6 +248,8 @@ func TestParseFlags(t *testing.T) {
"--akamai-client-token=o184671d5307a388180fbf7f11dbdf46",
"--akamai-client-secret=o184671d5307a388180fbf7f11dbdf46",
"--akamai-access-token=o184671d5307a388180fbf7f11dbdf46",
"--akamai-edgerc-path=/home/test/.edgerc",
"--akamai-edgerc-section=default",
"--infoblox-grid-host=127.0.0.1",
"--infoblox-wapi-port=8443",
"--infoblox-wapi-username=infoblox",
@ -335,6 +345,8 @@ func TestParseFlags(t *testing.T) {
"EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN": "o184671d5307a388180fbf7f11dbdf46",
"EXTERNAL_DNS_AKAMAI_CLIENT_SECRET": "o184671d5307a388180fbf7f11dbdf46",
"EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN": "o184671d5307a388180fbf7f11dbdf46",
"EXTERNAL_DNS_AKAMAI_EDGERC_PATH": "/home/test/.edgerc",
"EXTERNAL_DNS_AKAMAI_EDGERC_SECTION": "default",
"EXTERNAL_DNS_INFOBLOX_GRID_HOST": "127.0.0.1",
"EXTERNAL_DNS_INFOBLOX_WAPI_PORT": "8443",
"EXTERNAL_DNS_INFOBLOX_WAPI_USERNAME": "infoblox",

View File

@ -45,16 +45,16 @@ func ValidateConfig(cfg *externaldns.Config) error {
// Akamai provider specific validations
if cfg.Provider == "akamai" {
if cfg.AkamaiServiceConsumerDomain == "" {
if cfg.AkamaiServiceConsumerDomain == "" && cfg.AkamaiEdgercPath != "" {
return errors.New("no Akamai ServiceConsumerDomain specified")
}
if cfg.AkamaiClientToken == "" {
if cfg.AkamaiClientToken == "" && cfg.AkamaiEdgercPath != "" {
return errors.New("no Akamai client token specified")
}
if cfg.AkamaiClientSecret == "" {
if cfg.AkamaiClientSecret == "" && cfg.AkamaiEdgercPath != "" {
return errors.New("no Akamai client secret specified")
}
if cfg.AkamaiAccessToken == "" {
if cfg.AkamaiAccessToken == "" && cfg.AkamaiEdgercPath != "" {
return errors.New("no Akamai access token specified")
}
}
@ -86,6 +86,16 @@ 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 == "" {
return errors.New("--rfc2136-kerberos-username and --rfc2136-kerberos-password both required when specifying --rfc2136-gss-tsig option")
}
}
}
if cfg.IgnoreHostnameAnnotation && cfg.FQDNTemplate == "" {

View File

@ -150,3 +150,63 @@ func TestValidateGoodRfc2136Config(t *testing.T) {
assert.Nil(t, err)
}
func TestValidateBadRfc2136GssTsigConfig(t *testing.T) {
var invalidRfc2136GssTsigConfigs = []*externaldns.Config{
{
LogFormat: "json",
Sources: []string{"test-source"},
Provider: "rfc2136",
RFC2136GSSTSIG: true,
RFC2136KerberosUsername: "test-user",
RFC2136KerberosPassword: "",
RFC2136MinTTL: 3600,
},
{
LogFormat: "json",
Sources: []string{"test-source"},
Provider: "rfc2136",
RFC2136GSSTSIG: true,
RFC2136KerberosUsername: "",
RFC2136KerberosPassword: "test-pass",
RFC2136MinTTL: 3600,
},
{
LogFormat: "json",
Sources: []string{"test-source"},
Provider: "rfc2136",
RFC2136GSSTSIG: true,
RFC2136Insecure: true,
RFC2136KerberosUsername: "test-user",
RFC2136KerberosPassword: "test-pass",
RFC2136MinTTL: 3600,
},
}
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,
RFC2136KerberosUsername: "test-user",
RFC2136KerberosPassword: "test-pass",
RFC2136MinTTL: 3600,
},
}
for _, cfg := range validRfc2136GssTsigConfigs {
err := ValidateConfig(cfg)
assert.Nil(t, err)
}
}

View File

@ -43,6 +43,8 @@ type Plan struct {
DomainFilter endpoint.DomainFilter
// Property comparator compares custom properties of providers
PropertyComparator PropertyComparator
// DNS record types that will be considered for management
ManagedRecords []string
}
// Changes holds lists of actions to be executed by dns providers
@ -119,10 +121,10 @@ func (t planTable) addCandidate(e *endpoint.Endpoint) {
func (p *Plan) Calculate() *Plan {
t := newPlanTable()
for _, current := range filterRecordsForPlan(p.Current, p.DomainFilter) {
for _, current := range filterRecordsForPlan(p.Current, p.DomainFilter, p.ManagedRecords) {
t.addCurrent(current)
}
for _, desired := range filterRecordsForPlan(p.Desired, p.DomainFilter) {
for _, desired := range filterRecordsForPlan(p.Desired, p.DomainFilter, p.ManagedRecords) {
t.addCandidate(desired)
}
@ -155,9 +157,10 @@ func (p *Plan) Calculate() *Plan {
}
plan := &Plan{
Current: p.Current,
Desired: p.Desired,
Changes: changes,
Current: p.Current,
Desired: p.Desired,
Changes: changes,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
return plan
@ -194,12 +197,6 @@ func (p *Plan) shouldUpdateProviderSpecific(desired, current *endpoint.Endpoint)
}
if current.ProviderSpecific != nil {
for _, c := range current.ProviderSpecific {
// don't consider target health when detecting changes
// see: https://github.com/kubernetes-sigs/external-dns/issues/869#issuecomment-458576954
if c.Name == "aws/evaluate-target-health" {
continue
}
if d, ok := desiredProperties[c.Name]; ok {
if p.PropertyComparator != nil {
if !p.PropertyComparator(c.Name, c.Value, d.Value) {
@ -230,7 +227,7 @@ 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) []*endpoint.Endpoint {
func filterRecordsForPlan(records []*endpoint.Endpoint, domainFilter endpoint.DomainFilter, managedRecords []string) []*endpoint.Endpoint {
filtered := []*endpoint.Endpoint{}
for _, record := range records {
@ -238,14 +235,8 @@ func filterRecordsForPlan(records []*endpoint.Endpoint, domainFilter endpoint.Do
if !domainFilter.Match(record.DNSName) {
continue
}
// Explicitly specify which records we want to use for planning.
// TODO: Add AAAA records as well when they are supported.
switch record.RecordType {
case endpoint.RecordTypeA, endpoint.RecordTypeCNAME, endpoint.RecordTypeNS:
if isManagedRecord(record.RecordType, managedRecords) {
filtered = append(filtered, record)
default:
continue
}
}
@ -286,3 +277,12 @@ func CompareBoolean(defaultValue bool, name, current, previous string) bool {
return v1 == v2
}
func isManagedRecord(record string, managedRecords []string) bool {
for _, r := range managedRecords {
if record == r {
return true
}
}
return false
}

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",
@ -205,9 +214,10 @@ func (suite *PlanTestSuite) TestSyncFirstRound() {
expectedDelete := []*endpoint.Endpoint{}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
changes := p.Calculate().Changes
@ -226,9 +236,10 @@ func (suite *PlanTestSuite) TestSyncSecondRound() {
expectedDelete := []*endpoint.Endpoint{}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
changes := p.Calculate().Changes
@ -247,9 +258,10 @@ func (suite *PlanTestSuite) TestSyncSecondRoundMigration() {
expectedDelete := []*endpoint.Endpoint{}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
changes := p.Calculate().Changes
@ -268,9 +280,10 @@ func (suite *PlanTestSuite) TestSyncSecondRoundWithTTLChange() {
expectedDelete := []*endpoint.Endpoint{}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
changes := p.Calculate().Changes
@ -289,9 +302,10 @@ func (suite *PlanTestSuite) TestSyncSecondRoundWithProviderSpecificChange() {
expectedDelete := []*endpoint.Endpoint{}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
changes := p.Calculate().Changes
@ -316,6 +330,7 @@ func (suite *PlanTestSuite) TestSyncSecondRoundWithProviderSpecificDefaultFalse(
PropertyComparator: func(name, previous, current string) bool {
return CompareBoolean(false, name, previous, current)
},
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
changes := p.Calculate().Changes
@ -368,9 +383,10 @@ func (suite *PlanTestSuite) TestSyncSecondRoundWithOwnerInherited() {
expectedDelete := []*endpoint.Endpoint{}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
changes := p.Calculate().Changes
@ -410,9 +426,10 @@ func (suite *PlanTestSuite) TestDifferentTypes() {
expectedDelete := []*endpoint.Endpoint{}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
changes := p.Calculate().Changes
@ -430,6 +447,28 @@ func (suite *PlanTestSuite) TestIgnoreTXT() {
expectedUpdateNew := []*endpoint.Endpoint{}
expectedDelete := []*endpoint.Endpoint{}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
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) 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,
@ -452,9 +491,10 @@ func (suite *PlanTestSuite) TestRemoveEndpoint() {
expectedDelete := []*endpoint.Endpoint{suite.bar192A}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
changes := p.Calculate().Changes
@ -473,9 +513,10 @@ func (suite *PlanTestSuite) TestRemoveEndpointWithUpsert() {
expectedDelete := []*endpoint.Endpoint{}
p := &Plan{
Policies: []Policy{&UpsertOnlyPolicy{}},
Current: current,
Desired: desired,
Policies: []Policy{&UpsertOnlyPolicy{}},
Current: current,
Desired: desired,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
changes := p.Calculate().Changes
@ -495,9 +536,10 @@ func (suite *PlanTestSuite) TestDuplicatedEndpointsForSameResourceReplace() {
expectedDelete := []*endpoint.Endpoint{suite.bar192A}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
changes := p.Calculate().Changes
@ -518,9 +560,10 @@ func (suite *PlanTestSuite) TestDuplicatedEndpointsForSameResourceRetain() {
expectedDelete := []*endpoint.Endpoint{suite.bar192A}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
changes := p.Calculate().Changes
@ -540,9 +583,10 @@ func (suite *PlanTestSuite) TestMultipleRecordsSameNameDifferentSetIdentifier()
expectedDelete := []*endpoint.Endpoint{}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
changes := p.Calculate().Changes
@ -562,9 +606,10 @@ func (suite *PlanTestSuite) TestSetIdentifierUpdateCreatesAndDeletes() {
expectedDelete := []*endpoint.Endpoint{suite.multiple2}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
changes := p.Calculate().Changes
@ -584,10 +629,11 @@ func (suite *PlanTestSuite) TestDomainFiltersInitial() {
expectedDelete := []*endpoint.Endpoint{}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
DomainFilter: endpoint.NewDomainFilterWithExclusions([]string{"domain.tld"}, []string{"ex.domain.tld"}),
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
DomainFilter: endpoint.NewDomainFilterWithExclusions([]string{"domain.tld"}, []string{"ex.domain.tld"}),
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
changes := p.Calculate().Changes
@ -607,10 +653,11 @@ func (suite *PlanTestSuite) TestDomainFiltersUpdate() {
expectedDelete := []*endpoint.Endpoint{}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
DomainFilter: endpoint.NewDomainFilterWithExclusions([]string{"domain.tld"}, []string{"ex.domain.tld"}),
Policies: []Policy{&SyncPolicy{}},
Current: current,
Desired: desired,
DomainFilter: endpoint.NewDomainFilterWithExclusions([]string{"domain.tld"}, []string{"ex.domain.tld"}),
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
changes := p.Calculate().Changes
@ -686,3 +733,133 @@ func TestNormalizeDNSName(t *testing.T) {
assert.Equal(t, r.expect, gotName)
}
}
func TestShouldUpdateProviderSpecific(tt *testing.T) {
comparator := func(name, previous, current string) bool {
return previous == current
}
for _, test := range []struct {
name string
current *endpoint.Endpoint
desired *endpoint.Endpoint
propertyComparator func(name, previous, current string) bool
shouldUpdate bool
}{
{
name: "skip AWS target health",
current: &endpoint.Endpoint{
DNSName: "foo.com",
ProviderSpecific: []endpoint.ProviderSpecificProperty{
{Name: "aws/evaluate-target-health", Value: "true"},
},
},
desired: &endpoint.Endpoint{
DNSName: "bar.com",
ProviderSpecific: []endpoint.ProviderSpecificProperty{
{Name: "aws/evaluate-target-health", Value: "true"},
},
},
propertyComparator: comparator,
shouldUpdate: false,
},
{
name: "custom property unchanged",
current: &endpoint.Endpoint{
ProviderSpecific: []endpoint.ProviderSpecificProperty{
{Name: "custom/property", Value: "true"},
},
},
desired: &endpoint.Endpoint{
ProviderSpecific: []endpoint.ProviderSpecificProperty{
{Name: "custom/property", Value: "true"},
},
},
propertyComparator: comparator,
shouldUpdate: false,
},
{
name: "custom property value changed",
current: &endpoint.Endpoint{
ProviderSpecific: []endpoint.ProviderSpecificProperty{
{Name: "custom/property", Value: "true"},
},
},
desired: &endpoint.Endpoint{
ProviderSpecific: []endpoint.ProviderSpecificProperty{
{Name: "custom/property", Value: "false"},
},
},
propertyComparator: comparator,
shouldUpdate: true,
},
{
name: "custom property key changed",
current: &endpoint.Endpoint{
ProviderSpecific: []endpoint.ProviderSpecificProperty{
{Name: "custom/property", Value: "true"},
},
},
desired: &endpoint.Endpoint{
ProviderSpecific: []endpoint.ProviderSpecificProperty{
{Name: "new/property", Value: "true"},
},
},
propertyComparator: comparator,
shouldUpdate: true,
},
{
name: "desired has same key and value as current but not comparator is set",
current: &endpoint.Endpoint{
ProviderSpecific: []endpoint.ProviderSpecificProperty{
{Name: "custom/property", Value: "true"},
},
},
desired: &endpoint.Endpoint{
ProviderSpecific: []endpoint.ProviderSpecificProperty{
{Name: "custom/property", Value: "true"},
},
},
shouldUpdate: false,
},
{
name: "desired has same key and different value as current but not comparator is set",
current: &endpoint.Endpoint{
ProviderSpecific: []endpoint.ProviderSpecificProperty{
{Name: "custom/property", Value: "true"},
},
},
desired: &endpoint.Endpoint{
ProviderSpecific: []endpoint.ProviderSpecificProperty{
{Name: "custom/property", Value: "false"},
},
},
shouldUpdate: true,
},
{
name: "desired has different key from current but not comparator is set",
current: &endpoint.Endpoint{
ProviderSpecific: []endpoint.ProviderSpecificProperty{
{Name: "custom/property", Value: "true"},
},
},
desired: &endpoint.Endpoint{
ProviderSpecific: []endpoint.ProviderSpecificProperty{
{Name: "new/property", Value: "true"},
},
},
shouldUpdate: true,
},
} {
tt.Run(test.name, func(t *testing.T) {
plan := &Plan{
Current: []*endpoint.Endpoint{test.current},
Desired: []*endpoint.Endpoint{test.desired},
PropertyComparator: test.propertyComparator,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
b := plan.shouldUpdateProviderSpecific(test.desired, test.current)
assert.Equal(t, test.shouldUpdate, b)
})
}
}

View File

@ -17,15 +17,13 @@ limitations under the License.
package akamai
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
c "github.com/akamai/AkamaiOPEN-edgegrid-golang/client-v1"
dns "github.com/akamai/AkamaiOPEN-edgegrid-golang/configdns-v2"
"github.com/akamai/AkamaiOPEN-edgegrid-golang/edgegrid"
log "github.com/sirupsen/logrus"
@ -34,22 +32,23 @@ import (
"sigs.k8s.io/external-dns/provider"
)
type akamaiClient interface {
NewRequest(config edgegrid.Config, method, path string, body io.Reader) (*http.Request, error)
Do(config edgegrid.Config, req *http.Request) (*http.Response, error)
const (
// Default Record TTL
edgeDNSRecordTTL = 600
maxUint = ^uint(0)
maxInt = int(maxUint >> 1)
)
// edgeDNSClient is a proxy interface of the Akamai edgegrid configdns-v2 package that can be stubbed for testing.
type AkamaiDNSService interface {
ListZones(queryArgs dns.ZoneListQueryArgs) (*dns.ZoneListResponse, error)
GetRecordsets(zone string, queryArgs dns.RecordsetQueryArgs) (*dns.RecordSetResponse, error)
GetRecord(zone string, name string, recordtype string) (*dns.RecordBody, error)
DeleteRecord(record *dns.RecordBody, zone string, recLock bool) error
UpdateRecord(record *dns.RecordBody, zone string, recLock bool) error
CreateRecordsets(recordsets *dns.Recordsets, zone string, recLock bool) error
}
type akamaiOpenClient struct{}
func (*akamaiOpenClient) NewRequest(config edgegrid.Config, method, path string, body io.Reader) (*http.Request, error) {
return c.NewRequest(config, method, path, body)
}
func (*akamaiOpenClient) Do(config edgegrid.Config, req *http.Request) (*http.Response, error) {
return c.Do(config, req)
}
// AkamaiConfig clarifies the method signature
type AkamaiConfig struct {
DomainFilter endpoint.DomainFilter
ZoneIDFilter provider.ZoneIDFilter
@ -57,17 +56,25 @@ type AkamaiConfig struct {
ClientToken string
ClientSecret string
AccessToken string
EdgercPath string
EdgercSection string
MaxBody int
AccountKey string
DryRun bool
}
// AkamaiProvider implements the DNS provider for Akamai.
type AkamaiProvider struct {
provider.BaseProvider
// Edgedns zones to filter on
domainFilter endpoint.DomainFilter
// Contract Ids to filter on
zoneIDFilter provider.ZoneIDFilter
config edgegrid.Config
dryRun bool
client akamaiClient
// Edgegrid library configuration
config *edgegrid.Config
dryRun bool
// Defines client. Allows for mocking.
client AkamaiDNSService
}
type akamaiZones struct {
@ -79,84 +86,124 @@ type akamaiZone struct {
Zone string `json:"zone"`
}
type akamaiRecordsets struct {
Recordsets []akamaiRecord `json:"recordsets"`
}
type akamaiRecord struct {
Name string `json:"name"`
Type string `json:"type"`
TTL int64 `json:"ttl"`
Rdata []interface{} `json:"rdata"`
}
// NewAkamaiProvider initializes a new Akamai DNS based Provider.
func NewAkamaiProvider(akamaiConfig AkamaiConfig) *AkamaiProvider {
edgeGridConfig := edgegrid.Config{
Host: akamaiConfig.ServiceConsumerDomain,
ClientToken: akamaiConfig.ClientToken,
ClientSecret: akamaiConfig.ClientSecret,
AccessToken: akamaiConfig.AccessToken,
MaxBody: 1024,
HeaderToSign: []string{
"X-External-DNS",
},
Debug: false,
func NewAkamaiProvider(akamaiConfig AkamaiConfig, akaService AkamaiDNSService) (provider.Provider, error) {
var edgeGridConfig edgegrid.Config
/*
log.Debugf("Host: %s", akamaiConfig.ServiceConsumerDomain)
log.Debugf("ClientToken: %s", akamaiConfig.ClientToken)
log.Debugf("ClientSecret: %s", akamaiConfig.ClientSecret)
log.Debugf("AccessToken: %s", akamaiConfig.AccessToken)
log.Debugf("EdgePath: %s", akamaiConfig.EdgercPath)
log.Debugf("EdgeSection: %s", akamaiConfig.EdgercSection)
*/
// environment overrides edgerc file but config needs to be complete
if akamaiConfig.ServiceConsumerDomain == "" || akamaiConfig.ClientToken == "" || akamaiConfig.ClientSecret == "" || akamaiConfig.AccessToken == "" {
// Kubernetes config incomplete or non existent. Can't mix and match.
// Look for Akamai environment or .edgerd creds
var err error
edgeGridConfig, err = edgegrid.Init(akamaiConfig.EdgercPath, akamaiConfig.EdgercSection) // use default .edgerc location and section
if err != nil {
log.Errorf("Edgegrid Init Failed")
return &AkamaiProvider{}, err // return empty provider for backward compatibility
}
edgeGridConfig.HeaderToSign = append(edgeGridConfig.HeaderToSign, "X-External-DNS")
} else {
// Use external-dns config
edgeGridConfig = edgegrid.Config{
Host: akamaiConfig.ServiceConsumerDomain,
ClientToken: akamaiConfig.ClientToken,
ClientSecret: akamaiConfig.ClientSecret,
AccessToken: akamaiConfig.AccessToken,
MaxBody: 131072, // same default val as used by Edgegrid
HeaderToSign: []string{
"X-External-DNS",
},
Debug: false,
}
// Check for edgegrid overrides
if envval, ok := os.LookupEnv("AKAMAI_MAX_BODY"); ok {
if i, err := strconv.Atoi(envval); err == nil {
edgeGridConfig.MaxBody = i
log.Debugf("Edgegrid maxbody set to %s", envval)
}
}
if envval, ok := os.LookupEnv("AKAMAI_ACCOUNT_KEY"); ok {
edgeGridConfig.AccountKey = envval
log.Debugf("Edgegrid applying account key %s", envval)
}
if envval, ok := os.LookupEnv("AKAMAI_DEBUG"); ok {
if dbgval, err := strconv.ParseBool(envval); err == nil {
edgeGridConfig.Debug = dbgval
log.Debugf("Edgegrid debug set to %s", envval)
}
}
}
provider := &AkamaiProvider{
domainFilter: akamaiConfig.DomainFilter,
zoneIDFilter: akamaiConfig.ZoneIDFilter,
config: edgeGridConfig,
config: &edgeGridConfig,
dryRun: akamaiConfig.DryRun,
client: &akamaiOpenClient{},
}
return provider
if akaService != nil {
log.Debugf("Using STUB")
provider.client = akaService
} else {
provider.client = provider
}
// Init library for direct endpoint calls
dns.Init(edgeGridConfig)
return provider, nil
}
func (p *AkamaiProvider) request(method, path string, body io.Reader) (*http.Response, error) {
req, err := p.client.NewRequest(p.config, method, fmt.Sprintf("https://%s/%s", p.config.Host, path), body)
if err != nil {
log.Errorf("Akamai client failed to prepare the request")
return nil, err
}
resp, err := p.client.Do(p.config, req)
if err != nil {
log.Errorf("Akamai client failed to do the request")
return nil, err
}
if !c.IsSuccess(resp) {
return nil, c.NewAPIError(resp)
}
return resp, err
func (p AkamaiProvider) ListZones(queryArgs dns.ZoneListQueryArgs) (*dns.ZoneListResponse, error) {
return dns.ListZones(queryArgs)
}
//Look here for endpoint documentation -> https://developer.akamai.com/api/web_performance/fast_dns_zone_management/v2.html#getzones
func (p *AkamaiProvider) fetchZones() (zones akamaiZones, err error) {
log.Debugf("Trying to fetch zones from Akamai")
resp, err := p.request("GET", "config-dns/v2/zones?showAll=true&types=primary%2Csecondary", nil)
func (p AkamaiProvider) GetRecordsets(zone string, queryArgs dns.RecordsetQueryArgs) (*dns.RecordSetResponse, error) {
return dns.GetRecordsets(zone, queryArgs)
}
func (p AkamaiProvider) CreateRecordsets(recordsets *dns.Recordsets, zone string, reclock bool) error {
return recordsets.Save(zone, reclock)
}
func (p AkamaiProvider) GetRecord(zone string, name string, recordtype string) (*dns.RecordBody, error) {
return dns.GetRecord(zone, name, recordtype)
}
func (p AkamaiProvider) DeleteRecord(record *dns.RecordBody, zone string, recLock bool) error {
return record.Delete(zone, recLock)
}
func (p AkamaiProvider) UpdateRecord(record *dns.RecordBody, zone string, recLock bool) error {
return record.Update(zone, recLock)
}
// Fetch zones using Edgegrid DNS v2 API
func (p AkamaiProvider) fetchZones() (akamaiZones, error) {
filteredZones := akamaiZones{Zones: make([]akamaiZone, 0)}
queryArgs := dns.ZoneListQueryArgs{Types: "primary", ShowAll: true}
// filter based on contractIds
if len(p.zoneIDFilter.ZoneIDs) > 0 {
queryArgs.ContractIds = strings.Join(p.zoneIDFilter.ZoneIDs, ",")
}
resp, err := p.client.ListZones(queryArgs) // retrieve all primary zones filtered by contract ids
if err != nil {
log.Errorf("Failed to fetch zones from Akamai")
return zones, err
return filteredZones, err
}
err = json.NewDecoder(resp.Body).Decode(&zones)
if err != nil {
log.Errorf("Could not decode json response from Akamai on zone request")
return zones, err
}
defer resp.Body.Close()
filteredZones := akamaiZones{}
for _, zone := range zones.Zones {
if !p.zoneIDFilter.Match(zone.ContractID) {
log.Debugf("Skipping zone: '%s' with ZoneID: '%s', it does not match against ZoneID filters", zone.Zone, zone.ContractID)
continue
for _, zone := range resp.Zones {
if p.domainFilter.Match(zone.Zone) || !p.domainFilter.IsConfigured() {
filteredZones.Zones = append(filteredZones.Zones, akamaiZone{ContractID: zone.ContractId, Zone: zone.Zone})
log.Debugf("Fetched zone: '%s' (ZoneID: %s)", zone.Zone, zone.ContractId)
}
filteredZones.Zones = append(filteredZones.Zones, akamaiZone{ContractID: zone.ContractID, Zone: zone.Zone})
log.Debugf("Fetched zone: '%s' (ZoneID: %s)", zone.Zone, zone.ContractID)
}
lenFilteredZones := len(filteredZones.Zones)
if lenFilteredZones == 0 {
@ -168,53 +215,39 @@ func (p *AkamaiProvider) fetchZones() (zones akamaiZones, err error) {
return filteredZones, nil
}
//Look here for endpoint documentation -> https://developer.akamai.com/api/web_performance/fast_dns_zone_management/v2.html#getzonerecordsets
func (p *AkamaiProvider) fetchRecordSet(zone string) (recordSet akamaiRecordsets, err error) {
log.Debugf("Trying to fetch endpoints for zone: '%s' from Akamai", zone)
resp, err := p.request("GET", "config-dns/v2/zones/"+zone+"/recordsets?showAll=true&types=A%2CTXT%2CCNAME", nil)
if err != nil {
log.Errorf("Failed to fetch records from Akamai for zone: '%s'", zone)
return recordSet, err
}
defer resp.Body.Close()
err = json.NewDecoder(resp.Body).Decode(&recordSet)
if err != nil {
log.Errorf("Could not decode json response from Akamai for zone: '%s' on request", zone)
return recordSet, err
}
return recordSet, nil
}
//Records returns the list of records in a given zone.
func (p *AkamaiProvider) Records(context.Context) (endpoints []*endpoint.Endpoint, err error) {
zones, err := p.fetchZones()
func (p AkamaiProvider) Records(context.Context) (endpoints []*endpoint.Endpoint, err error) {
zones, err := p.fetchZones() // returns a filtered set of zones
if err != nil {
log.Warnf("No zones to fetch endpoints from!")
log.Warnf("Failed to identify target zones! Error: %s", err.Error())
return endpoints, err
}
for _, zone := range zones.Zones {
records, err := p.fetchRecordSet(zone.Zone)
recordsets, err := p.client.GetRecordsets(zone.Zone, dns.RecordsetQueryArgs{})
if err != nil {
log.Warnf("No recordsets could be fetched for zone: '%s'!", zone.Zone)
log.Errorf("Recordsets retrieval for zone: '%s' failed! %s", zone.Zone, err.Error())
continue
}
if len(recordsets.Recordsets) == 0 {
log.Warnf("Zone %s contains no recordsets", zone.Zone)
}
for _, record := range records.Recordsets {
rdata := make([]string, len(record.Rdata))
for i, v := range record.Rdata {
rdata[i] = v.(string)
}
if !p.domainFilter.Match(record.Name) {
log.Debugf("Skipping endpoint DNSName: '%s' RecordType: '%s', it does not match against Domain filters", record.Name, record.Type)
for _, recordset := range recordsets.Recordsets {
if !provider.SupportedRecordType(recordset.Type) {
log.Debugf("Skipping endpoint DNSName: '%s' RecordType: '%s'. Record type not supported.", recordset.Name, recordset.Type)
continue
}
endpoints = append(endpoints, endpoint.NewEndpoint(record.Name, record.Type, rdata...))
log.Debugf("Fetched endpoint DNSName: '%s' RecordType: '%s' Rdata: '%s')", record.Name, record.Type, rdata)
if !p.domainFilter.Match(recordset.Name) {
log.Debugf("Skipping endpoint. Record name %s doesn't match containing zone %s.", recordset.Name, zone)
continue
}
var temp interface{} = int64(recordset.TTL)
var ttl endpoint.TTL = endpoint.TTL(temp.(int64))
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(recordset.Name,
recordset.Type,
ttl,
trimTxtRdata(recordset.Rdata, recordset.Type)...))
log.Debugf("Fetched endpoint DNSName: '%s' RecordType: '%s' Rdata: '%s')", recordset.Name, recordset.Type, recordset.Rdata)
}
}
lenEndpoints := len(endpoints)
@ -222,161 +255,237 @@ func (p *AkamaiProvider) Records(context.Context) (endpoints []*endpoint.Endpoin
log.Warnf("No endpoints could be fetched")
} else {
log.Debugf("Fetched '%d' endpoints from Akamai", lenEndpoints)
log.Debugf("Endpoints [%v]", endpoints)
}
return endpoints, nil
}
// ApplyChanges applies a given set of changes in a given zone.
func (p *AkamaiProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
func (p AkamaiProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
zoneNameIDMapper := provider.ZoneIDName{}
zones, err := p.fetchZones()
if err != nil {
log.Warnf("No zones to fetch endpoints from!")
return nil
log.Errorf("Failed to fetch zones from Akamai")
return err
}
for _, z := range zones.Zones {
zoneNameIDMapper[z.Zone] = z.Zone
}
log.Debugf("Processing zones: [%v]", zoneNameIDMapper)
_, cf := p.createRecords(zoneNameIDMapper, changes.Create)
if !p.dryRun {
if len(cf) > 0 {
log.Warnf("Not all desired endpoints could be created, retrying next iteration")
for _, f := range cf {
log.Warnf("Not created was DNSName: '%s' RecordType: '%s'", f.DNSName, f.RecordType)
// Create recordsets
log.Debugf("Create Changes requested [%v]", changes.Create)
if err := p.createRecordsets(zoneNameIDMapper, changes.Create); err != nil {
return err
}
// Delete recordsets
log.Debugf("Delete Changes requested [%v]", changes.Delete)
if err := p.deleteRecordsets(zoneNameIDMapper, changes.Delete); err != nil {
return err
}
// Update recordsets
log.Debugf("Update Changes requested [%v]", changes.UpdateNew)
if err := p.updateNewRecordsets(zoneNameIDMapper, changes.UpdateNew); err != nil {
return err
}
// Check that all old endpoints were accounted for
revRecs := changes.Delete
revRecs = append(revRecs, changes.UpdateNew...)
for _, rec := range changes.UpdateOld {
found := false
for _, r := range revRecs {
if rec.DNSName == r.DNSName {
found = true
break
}
}
}
_, df := p.deleteRecords(zoneNameIDMapper, changes.Delete)
if !p.dryRun {
if len(df) > 0 {
log.Warnf("Not all endpoints that require deletion could be deleted, retrying next iteration")
for _, f := range df {
log.Warnf("Not deleted was DNSName: '%s' RecordType: '%s'", f.DNSName, f.RecordType)
}
}
}
_, uf := p.updateNewRecords(zoneNameIDMapper, changes.UpdateNew)
if !p.dryRun {
if len(uf) > 0 {
log.Warnf("Not all endpoints that require updating could be updated, retrying next iteration")
for _, f := range uf {
log.Warnf("Not updated was DNSName: '%s' RecordType: '%s'", f.DNSName, f.RecordType)
}
}
}
for _, uold := range changes.UpdateOld {
if !p.dryRun {
log.Debugf("UpdateOld (ignored) for DNSName: '%s' RecordType: '%s'", uold.DNSName, uold.RecordType)
if !found {
log.Warnf("UpdateOld endpoint '%s' is not accounted for in UpdateNew|Delete endpoint list", rec.DNSName)
}
}
return nil
}
func (p *AkamaiProvider) newAkamaiRecord(dnsName, recordType string, targets ...string) *akamaiRecord {
cleanTargets := make([]interface{}, len(targets))
for idx, target := range targets {
cleanTargets[idx] = strings.TrimSuffix(target, ".")
}
return &akamaiRecord{
// Create DNS Recordset
func newAkamaiRecordset(dnsName, recordType string, ttl int, targets []string) dns.Recordset {
return dns.Recordset{
Name: strings.TrimSuffix(dnsName, "."),
Rdata: cleanTargets,
Rdata: targets,
Type: recordType,
TTL: 300,
TTL: ttl,
}
}
func (p *AkamaiProvider) createRecords(zoneNameIDMapper provider.ZoneIDName, endpoints []*endpoint.Endpoint) (created []*endpoint.Endpoint, failed []*endpoint.Endpoint) {
for _, endpoint := range endpoints {
if !p.domainFilter.Match(endpoint.DNSName) {
log.Debugf("Skipping creation at Akamai of endpoint DNSName: '%s' RecordType: '%s', it does not match against Domain filters", endpoint.DNSName, endpoint.RecordType)
// cleanTargets preps recordset rdata if necessary for EdgeDNS
func cleanTargets(rtype string, targets ...string) []string {
log.Debugf("Targets to clean: [%v]", targets)
if rtype == "CNAME" || rtype == "SRV" {
for idx, target := range targets {
targets[idx] = strings.TrimSuffix(target, ".")
}
} else if rtype == "TXT" {
for idx, target := range targets {
log.Debugf("TXT data to clean: [%s]", target)
// need to embed text data in quotes. Make sure not piling on
target = strings.Trim(target, "\"")
// bug in DNS API with embedded quotes.
if strings.Contains(target, "owner") && strings.Contains(target, "\"") {
target = strings.ReplaceAll(target, "\"", "`")
}
targets[idx] = "\"" + target + "\""
}
}
log.Debugf("Clean targets: [%v]", targets)
return targets
}
// trimTxtRdata removes surrounding quotes for received TXT rdata
func trimTxtRdata(rdata []string, rtype string) []string {
if rtype == "TXT" {
for idx, d := range rdata {
if strings.Contains(d, "`") {
rdata[idx] = strings.ReplaceAll(d, "`", "\"")
}
}
}
log.Debugf("Trimmed data: [%v]", rdata)
return rdata
}
func ttlAsInt(src endpoint.TTL) int {
var temp interface{} = int64(src)
var temp64 = temp.(int64)
var ttl int = edgeDNSRecordTTL
if temp64 > 0 && temp64 <= int64(maxInt) {
ttl = int(temp64)
}
return ttl
}
// Create Endpoint Recordsets
func (p AkamaiProvider) createRecordsets(zoneNameIDMapper provider.ZoneIDName, endpoints []*endpoint.Endpoint) error {
if len(endpoints) == 0 {
log.Info("No endpoints to create")
return nil
}
endpointsByZone := edgeChangesByZone(zoneNameIDMapper, endpoints)
// create all recordsets by zone
for zone, endpoints := range endpointsByZone {
recordsets := &dns.Recordsets{Recordsets: make([]dns.Recordset, 0)}
for _, endpoint := range endpoints {
newrec := newAkamaiRecordset(endpoint.DNSName,
endpoint.RecordType,
ttlAsInt(endpoint.RecordTTL),
cleanTargets(endpoint.RecordType, endpoint.Targets...))
logfields := log.Fields{
"record": newrec.Name,
"type": newrec.Type,
"ttl": newrec.TTL,
"target": fmt.Sprintf("%v", newrec.Rdata),
"zone": zone,
}
log.WithFields(logfields).Info("Creating recordsets")
recordsets.Recordsets = append(recordsets.Recordsets, newrec)
}
if p.dryRun {
continue
}
if zoneName, _ := zoneNameIDMapper.FindZone(endpoint.DNSName); zoneName != "" {
akamaiRecord := p.newAkamaiRecord(endpoint.DNSName, endpoint.RecordType, endpoint.Targets...)
body, _ := json.MarshalIndent(akamaiRecord, "", " ")
log.Infof("Create new Endpoint at Akamai FastDNS - Zone: '%s', DNSName: '%s', RecordType: '%s', Targets: '%+v'", zoneName, endpoint.DNSName, endpoint.RecordType, endpoint.Targets)
if p.dryRun {
continue
}
_, err := p.request("POST", "config-dns/v2/zones/"+zoneName+"/names/"+endpoint.DNSName+"/types/"+endpoint.RecordType, bytes.NewReader(body))
if err != nil {
log.Errorf("Failed to create Akamai endpoint DNSName: '%s' RecordType: '%s' for zone: '%s'", endpoint.DNSName, endpoint.RecordType, zoneName)
failed = append(failed, endpoint)
continue
}
created = append(created, endpoint)
} else {
log.Warnf("No matching zone for endpoint addition DNSName: '%s' RecordType: '%s'", endpoint.DNSName, endpoint.RecordType)
failed = append(failed, endpoint)
// Create recordsets all at once
err := p.client.CreateRecordsets(recordsets, zone, true)
if err != nil {
log.Errorf("Failed to create endpoints for DNS zone %s. Error: %s", zone, err.Error())
return err
}
}
return created, failed
return nil
}
func (p *AkamaiProvider) deleteRecords(zoneNameIDMapper provider.ZoneIDName, endpoints []*endpoint.Endpoint) (deleted []*endpoint.Endpoint, failed []*endpoint.Endpoint) {
func (p AkamaiProvider) deleteRecordsets(zoneNameIDMapper provider.ZoneIDName, endpoints []*endpoint.Endpoint) error {
for _, endpoint := range endpoints {
if !p.domainFilter.Match(endpoint.DNSName) {
log.Debugf("Skipping deletion at Akamai of endpoint: '%s' type: '%s', it does not match against Domain filters", endpoint.DNSName, endpoint.RecordType)
zoneName, _ := zoneNameIDMapper.FindZone(endpoint.DNSName)
if zoneName == "" {
log.Debugf("Skipping Akamai Edge DNS endpoint deletion: '%s' type: '%s', it does not match against Domain filters", endpoint.DNSName, endpoint.RecordType)
continue
}
if zoneName, _ := zoneNameIDMapper.FindZone(endpoint.DNSName); zoneName != "" {
log.Infof("Deletion at Akamai FastDNS - Zone: '%s', DNSName: '%s', RecordType: '%s', Targets: '%+v'", zoneName, endpoint.DNSName, endpoint.RecordType, endpoint.Targets)
log.Infof("Akamai Edge DNS recordset deletion- Zone: '%s', DNSName: '%s', RecordType: '%s', Targets: '%+v'", zoneName, endpoint.DNSName, endpoint.RecordType, endpoint.Targets)
if p.dryRun {
continue
}
_, err := p.request("DELETE", "config-dns/v2/zones/"+zoneName+"/names/"+endpoint.DNSName+"/types/"+endpoint.RecordType, nil)
if err != nil {
log.Errorf("Failed to delete Akamai endpoint DNSName: '%s' for zone: '%s'", endpoint.DNSName, zoneName)
failed = append(failed, endpoint)
continue
}
deleted = append(deleted, endpoint)
} else {
log.Warnf("No matching zone for endpoint deletion DNSName: '%s' RecordType: '%s'", endpoint.DNSName, endpoint.RecordType)
failed = append(failed, endpoint)
}
}
return deleted, failed
}
func (p *AkamaiProvider) updateNewRecords(zoneNameIDMapper provider.ZoneIDName, endpoints []*endpoint.Endpoint) (updated []*endpoint.Endpoint, failed []*endpoint.Endpoint) {
for _, endpoint := range endpoints {
if !p.domainFilter.Match(endpoint.DNSName) {
log.Debugf("Skipping update at Akamai of endpoint DNSName: '%s' RecordType: '%s', it does not match against Domain filters", endpoint.DNSName, endpoint.RecordType)
if p.dryRun {
continue
}
if zoneName, _ := zoneNameIDMapper.FindZone(endpoint.DNSName); zoneName != "" {
akamaiRecord := p.newAkamaiRecord(endpoint.DNSName, endpoint.RecordType, endpoint.Targets...)
body, _ := json.MarshalIndent(akamaiRecord, "", " ")
log.Infof("Updating endpoint at Akamai FastDNS - Zone: '%s', DNSName: '%s', RecordType: '%s', Targets: '%+v'", zoneName, endpoint.DNSName, endpoint.RecordType, endpoint.Targets)
if p.dryRun {
continue
recName := strings.TrimSuffix(endpoint.DNSName, ".")
rec, err := p.client.GetRecord(zoneName, recName, endpoint.RecordType)
if err != nil {
if _, ok := err.(*dns.RecordError); !ok {
return fmt.Errorf("endpoint deletion. record validation failed. error: %s", err.Error())
}
_, err := p.request("PUT", "config-dns/v2/zones/"+zoneName+"/names/"+endpoint.DNSName+"/types/"+endpoint.RecordType, bytes.NewReader(body))
if err != nil {
log.Errorf("Failed to update Akamai endpoint DNSName: '%s' for zone: '%s'", endpoint.DNSName, zoneName)
failed = append(failed, endpoint)
continue
}
updated = append(updated, endpoint)
} else {
log.Warnf("No matching zone for endpoint update DNSName: '%s' RecordType: '%s'", endpoint.DNSName, endpoint.RecordType)
failed = append(failed, endpoint)
log.Infof("Endpoint deletion. Record doesn't exist. Name: %s, Type: %s", recName, endpoint.RecordType)
continue
}
if err := p.client.DeleteRecord(rec, zoneName, true); err != nil {
log.Errorf("edge dns recordset deletion failed. error: %s", err.Error())
return err
}
}
return updated, failed
return nil
}
// Update endpoint recordsets
func (p AkamaiProvider) updateNewRecordsets(zoneNameIDMapper provider.ZoneIDName, endpoints []*endpoint.Endpoint) error {
for _, endpoint := range endpoints {
zoneName, _ := zoneNameIDMapper.FindZone(endpoint.DNSName)
if zoneName == "" {
log.Debugf("Skipping Akamai Edge DNS endpoint update: '%s' type: '%s', it does not match against Domain filters", endpoint.DNSName, endpoint.RecordType)
continue
}
log.Infof("Akamai Edge DNS recordset update - Zone: '%s', DNSName: '%s', RecordType: '%s', Targets: '%+v'", zoneName, endpoint.DNSName, endpoint.RecordType, endpoint.Targets)
if p.dryRun {
continue
}
recName := strings.TrimSuffix(endpoint.DNSName, ".")
rec, err := p.client.GetRecord(zoneName, recName, endpoint.RecordType)
if err != nil {
log.Errorf("Endpoint update. Record validation failed. Error: %s", err.Error())
return err
}
rec.TTL = ttlAsInt(endpoint.RecordTTL)
rec.Target = cleanTargets(endpoint.RecordType, endpoint.Targets...)
if err := p.client.UpdateRecord(rec, zoneName, true); err != nil {
log.Errorf("Akamai Edge DNS recordset update failed. Error: %s", err.Error())
return err
}
}
return nil
}
// edgeChangesByZone separates a multi-zone change into a single change per zone.
func edgeChangesByZone(zoneMap provider.ZoneIDName, endpoints []*endpoint.Endpoint) map[string][]*endpoint.Endpoint {
createsByZone := make(map[string][]*endpoint.Endpoint, len(zoneMap))
for _, z := range zoneMap {
createsByZone[z] = make([]*endpoint.Endpoint, 0)
}
for _, ep := range endpoints {
zone, _ := zoneMap.FindZone(ep.DNSName)
if zone != "" {
createsByZone[zone] = append(createsByZone[zone], ep)
continue
}
log.Debugf("Skipping Akamai Edge DNS creation of endpoint: '%s' type: '%s', it does not match against Domain filters", ep.DNSName, ep.RecordType)
}
return createsByZone
}

View File

@ -17,148 +17,206 @@ limitations under the License.
package akamai
import (
"bytes"
"context"
"encoding/json"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
log "github.com/sirupsen/logrus"
"testing"
"github.com/akamai/AkamaiOPEN-edgegrid-golang/edgegrid"
dns "github.com/akamai/AkamaiOPEN-edgegrid-golang/configdns-v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
)
type mockAkamaiClient struct {
mock.Mock
type edgednsStubData struct {
objType string // zone, record, recordsets
output []interface{}
updateRecords []interface{}
createRecords []interface{}
}
func (m *mockAkamaiClient) NewRequest(config edgegrid.Config, met, p string, b io.Reader) (*http.Request, error) {
switch {
case met == "GET":
switch {
case strings.HasPrefix(p, "https:///config-dns/v2/zones?"):
b = bytes.NewReader([]byte("{\"zones\":[{\"contractId\":\"Test\",\"zone\":\"example.com\"},{\"contractId\":\"Exclude-Me\",\"zone\":\"exclude.me\"}]}"))
case strings.HasPrefix(p, "https:///config-dns/v2/zones/example.com/"):
b = bytes.NewReader([]byte("{\"recordsets\":[{\"name\":\"www.example.com\",\"type\":\"A\",\"ttl\":300,\"rdata\":[\"10.0.0.2\",\"10.0.0.3\"]},{\"name\":\"www.example.com\",\"type\":\"TXT\",\"ttl\":300,\"rdata\":[\"heritage=external-dns,external-dns/owner=default\"]}]}"))
case strings.HasPrefix(p, "https:///config-dns/v2/zones/exclude.me/"):
b = bytes.NewReader([]byte("{\"recordsets\":[{\"name\":\"www.exclude.me\",\"type\":\"A\",\"ttl\":300,\"rdata\":[\"192.168.0.1\",\"192.168.0.2\"]}]}"))
}
case met == "DELETE":
b = bytes.NewReader([]byte("{\"title\": \"Success\", \"status\": 200, \"detail\": \"Record deleted\", \"requestId\": \"4321\"}"))
case met == "ERROR":
b = bytes.NewReader([]byte("{\"status\": 404 }"))
}
req := httptest.NewRequest(met, p, b)
return req, nil
type edgednsStub struct {
stubData map[string]edgednsStubData
}
func (m *mockAkamaiClient) Do(config edgegrid.Config, req *http.Request) (*http.Response, error) {
handler := func(w http.ResponseWriter, r *http.Request) (isError bool) {
b, _ := ioutil.ReadAll(r.Body)
io.WriteString(w, string(b))
return string(b) == "{\"status\": 404 }"
func newStub() *edgednsStub {
return &edgednsStub{
stubData: make(map[string]edgednsStubData),
}
w := httptest.NewRecorder()
err := handler(w, req)
resp := w.Result()
}
if err == true {
resp.StatusCode = 400
func createAkamaiStubProvider(stub *edgednsStub, domfilter endpoint.DomainFilter, idfilter provider.ZoneIDFilter) (*AkamaiProvider, error) {
akamaiConfig := AkamaiConfig{
DomainFilter: domfilter,
ZoneIDFilter: idfilter,
ServiceConsumerDomain: "testzone.com",
ClientToken: "test_token",
ClientSecret: "test_client_secret",
AccessToken: "test_access_token",
}
prov, err := NewAkamaiProvider(akamaiConfig, stub)
aprov := prov.(*AkamaiProvider)
return aprov, err
}
func (r *edgednsStub) createStubDataEntry(objtype string) {
log.Debugf("Creating stub data entry")
if _, exists := r.stubData[objtype]; !exists {
r.stubData[objtype] = edgednsStubData{objType: objtype}
}
return
}
func (r *edgednsStub) setOutput(objtype string, output []interface{}) {
log.Debugf("Setting output to %v", output)
r.createStubDataEntry(objtype)
stubdata := r.stubData[objtype]
stubdata.output = output
r.stubData[objtype] = stubdata
return
}
func (r *edgednsStub) setUpdateRecords(objtype string, records []interface{}) {
log.Debugf("Setting updaterecords to %v", records)
r.createStubDataEntry(objtype)
stubdata := r.stubData[objtype]
stubdata.updateRecords = records
r.stubData[objtype] = stubdata
return
}
func (r *edgednsStub) setCreateRecords(objtype string, records []interface{}) {
log.Debugf("Setting createrecords to %v", records)
r.createStubDataEntry(objtype)
stubdata := r.stubData[objtype]
stubdata.createRecords = records
r.stubData[objtype] = stubdata
return
}
func (r *edgednsStub) ListZones(queryArgs dns.ZoneListQueryArgs) (*dns.ZoneListResponse, error) {
log.Debugf("Entering ListZones")
// Ignore Metadata`
resp := &dns.ZoneListResponse{}
zones := make([]*dns.ZoneResponse, 0)
for _, zname := range r.stubData["zone"].output {
log.Debugf("Processing output: %v", zname)
zn := &dns.ZoneResponse{Zone: zname.(string), ContractId: "contract"}
log.Debugf("Created Zone Object: %v", zn)
zones = append(zones, zn)
}
resp.Zones = zones
return resp, nil
}
func (r *edgednsStub) GetRecordsets(zone string, queryArgs dns.RecordsetQueryArgs) (*dns.RecordSetResponse, error) {
log.Debugf("Entering GetRecordsets")
// Ignore Metadata`
resp := &dns.RecordSetResponse{}
sets := make([]dns.Recordset, 0)
for _, rec := range r.stubData["recordset"].output {
rset := rec.(dns.Recordset)
sets = append(sets, rset)
}
resp.Recordsets = sets
return resp, nil
}
func TestRequestError(t *testing.T) {
config := AkamaiConfig{}
func (r *edgednsStub) CreateRecordsets(recordsets *dns.Recordsets, zone string, reclock bool) error {
client := &mockAkamaiClient{}
c := NewAkamaiProvider(config)
c.client = client
m := "ERROR"
p := ""
b := ""
x, err := c.request(m, p, bytes.NewReader([]byte(b)))
assert.Nil(t, x)
assert.NotNil(t, err)
return nil
}
func TestFetchZonesZoneIDFilter(t *testing.T) {
config := AkamaiConfig{
ZoneIDFilter: provider.NewZoneIDFilter([]string{"Test"}),
}
func (r *edgednsStub) GetRecord(zone string, name string, record_type string) (*dns.RecordBody, error) {
client := &mockAkamaiClient{}
c := NewAkamaiProvider(config)
c.client = client
resp := &dns.RecordBody{}
return resp, nil
}
func (r *edgednsStub) DeleteRecord(record *dns.RecordBody, zone string, recLock bool) error {
return nil
}
func (r *edgednsStub) UpdateRecord(record *dns.RecordBody, zone string, recLock bool) error {
return nil
}
// Test FetchZones
func TestFetchZonesZoneIDFilter(t *testing.T) {
stub := newStub()
domfilter := endpoint.DomainFilter{}
idfilter := provider.NewZoneIDFilter([]string{"Test"})
c, err := createAkamaiStubProvider(stub, domfilter, idfilter)
assert.Nil(t, err)
stub.setOutput("zone", []interface{}{"test1.testzone.com", "test2.testzone.com"})
x, _ := c.fetchZones()
y, _ := json.Marshal(x)
if assert.NotNil(t, y) {
assert.Equal(t, "{\"zones\":[{\"contractId\":\"Test\",\"zone\":\"example.com\"}]}", string(y))
assert.Equal(t, "{\"zones\":[{\"contractId\":\"contract\",\"zone\":\"test1.testzone.com\"},{\"contractId\":\"contract\",\"zone\":\"test2.testzone.com\"}]}", string(y))
}
}
func TestFetchZonesEmpty(t *testing.T) {
config := AkamaiConfig{
DomainFilter: endpoint.NewDomainFilter([]string{"Nonexistent"}),
ZoneIDFilter: provider.NewZoneIDFilter([]string{"Nonexistent"}),
}
client := &mockAkamaiClient{}
c := NewAkamaiProvider(config)
c.client = client
stub := newStub()
domfilter := endpoint.NewDomainFilter([]string{"Nonexistent"})
idfilter := provider.NewZoneIDFilter([]string{"Nonexistent"})
c, err := createAkamaiStubProvider(stub, domfilter, idfilter)
assert.Nil(t, err)
stub.setOutput("zone", []interface{}{})
x, _ := c.fetchZones()
y, _ := json.Marshal(x)
if assert.NotNil(t, y) {
assert.Equal(t, "{\"zones\":null}", string(y))
}
}
func TestFetchRecordset1(t *testing.T) {
config := AkamaiConfig{}
client := &mockAkamaiClient{}
c := NewAkamaiProvider(config)
c.client = client
x, _ := c.fetchRecordSet("example.com")
y, _ := json.Marshal(x)
if assert.NotNil(t, y) {
assert.Equal(t, "{\"recordsets\":[{\"name\":\"www.example.com\",\"type\":\"A\",\"ttl\":300,\"rdata\":[\"10.0.0.2\",\"10.0.0.3\"]},{\"name\":\"www.example.com\",\"type\":\"TXT\",\"ttl\":300,\"rdata\":[\"heritage=external-dns,external-dns/owner=default\"]}]}", string(y))
}
}
func TestFetchRecordset2(t *testing.T) {
config := AkamaiConfig{}
client := &mockAkamaiClient{}
c := NewAkamaiProvider(config)
c.client = client
x, _ := c.fetchRecordSet("exclude.me")
y, _ := json.Marshal(x)
if assert.NotNil(t, y) {
assert.Equal(t, "{\"recordsets\":[{\"name\":\"www.exclude.me\",\"type\":\"A\",\"ttl\":300,\"rdata\":[\"192.168.0.1\",\"192.168.0.2\"]}]}", string(y))
assert.Equal(t, "{\"zones\":[]}", string(y))
}
}
// TestAkamaiRecords tests record endpoint
func TestAkamaiRecords(t *testing.T) {
config := AkamaiConfig{}
client := &mockAkamaiClient{}
c := NewAkamaiProvider(config)
c.client = client
stub := newStub()
domfilter := endpoint.DomainFilter{}
idfilter := provider.ZoneIDFilter{}
c, err := createAkamaiStubProvider(stub, domfilter, idfilter)
assert.Nil(t, err)
stub.setOutput("zone", []interface{}{"test1.testzone.com"})
recordsets := make([]interface{}, 0)
recordsets = append(recordsets, dns.Recordset{
Name: "www.example.com",
Type: endpoint.RecordTypeA,
Rdata: []string{"10.0.0.2", "10.0.0.3"},
})
recordsets = append(recordsets, dns.Recordset{
Name: "www.example.com",
Type: endpoint.RecordTypeTXT,
Rdata: []string{"heritage=external-dns,external-dns/owner=default"},
})
recordsets = append(recordsets, dns.Recordset{
Name: "www.exclude.me",
Type: endpoint.RecordTypeA,
Rdata: []string{"192.168.0.1", "192.168.0.2"},
})
stub.setOutput("recordset", recordsets)
endpoints := make([]*endpoint.Endpoint, 0)
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3"))
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"))
@ -171,28 +229,40 @@ func TestAkamaiRecords(t *testing.T) {
}
func TestAkamaiRecordsEmpty(t *testing.T) {
config := AkamaiConfig{
ZoneIDFilter: provider.NewZoneIDFilter([]string{"Nonexistent"}),
}
client := &mockAkamaiClient{}
c := NewAkamaiProvider(config)
c.client = client
stub := newStub()
domfilter := endpoint.DomainFilter{}
idfilter := provider.NewZoneIDFilter([]string{"Nonexistent"})
c, err := createAkamaiStubProvider(stub, domfilter, idfilter)
assert.Nil(t, err)
stub.setOutput("zone", []interface{}{"test1.testzone.com"})
recordsets := make([]interface{}, 0)
stub.setOutput("recordset", recordsets)
x, _ := c.Records(context.Background())
assert.Nil(t, x)
}
func TestAkamaiRecordsFilters(t *testing.T) {
config := AkamaiConfig{
DomainFilter: endpoint.NewDomainFilter([]string{"www.exclude.me"}),
ZoneIDFilter: provider.NewZoneIDFilter([]string{"Exclude-Me"}),
}
client := &mockAkamaiClient{}
c := NewAkamaiProvider(config)
c.client = client
stub := newStub()
domfilter := endpoint.NewDomainFilter([]string{"www.exclude.me"})
idfilter := provider.ZoneIDFilter{}
c, err := createAkamaiStubProvider(stub, domfilter, idfilter)
assert.Nil(t, err)
stub.setOutput("zone", []interface{}{"www.exclude.me"})
recordsets := make([]interface{}, 0)
recordsets = append(recordsets, dns.Recordset{
Name: "www.example.com",
Type: endpoint.RecordTypeA,
Rdata: []string{"10.0.0.2", "10.0.0.3"},
})
recordsets = append(recordsets, dns.Recordset{
Name: "www.exclude.me",
Type: endpoint.RecordTypeA,
Rdata: []string{"192.168.0.1", "192.168.0.2"},
})
stub.setOutput("recordset", recordsets)
endpoints := make([]*endpoint.Endpoint, 0)
endpoints = append(endpoints, endpoint.NewEndpoint("www.exclude.me", endpoint.RecordTypeA, "192.168.0.1", "192.168.0.2"))
@ -202,32 +272,32 @@ func TestAkamaiRecordsFilters(t *testing.T) {
}
}
// TestCreateRecords tests create function
// (p AkamaiProvider) createRecordsets(zoneNameIDMapper provider.ZoneIDName, endpoints []*endpoint.Endpoint) error
func TestCreateRecords(t *testing.T) {
config := AkamaiConfig{}
client := &mockAkamaiClient{}
c := NewAkamaiProvider(config)
c.client = client
stub := newStub()
domfilter := endpoint.DomainFilter{}
idfilter := provider.ZoneIDFilter{}
c, err := createAkamaiStubProvider(stub, domfilter, idfilter)
assert.Nil(t, err)
zoneNameIDMapper := provider.ZoneIDName{"example.com": "example.com"}
endpoints := make([]*endpoint.Endpoint, 0)
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3"))
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"))
x, _ := c.createRecords(zoneNameIDMapper, endpoints)
if assert.NotNil(t, x) {
assert.Equal(t, endpoints, x)
}
err = c.createRecordsets(zoneNameIDMapper, endpoints)
assert.Nil(t, err)
}
func TestCreateRecordsDomainFilter(t *testing.T) {
config := AkamaiConfig{
DomainFilter: endpoint.NewDomainFilter([]string{"example.com"}),
}
client := &mockAkamaiClient{}
c := NewAkamaiProvider(config)
c.client = client
stub := newStub()
domfilter := endpoint.DomainFilter{}
idfilter := provider.ZoneIDFilter{}
c, err := createAkamaiStubProvider(stub, domfilter, idfilter)
assert.Nil(t, err)
zoneNameIDMapper := provider.ZoneIDName{"example.com": "example.com"}
endpoints := make([]*endpoint.Endpoint, 0)
@ -235,38 +305,36 @@ func TestCreateRecordsDomainFilter(t *testing.T) {
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"))
exclude := append(endpoints, endpoint.NewEndpoint("www.exclude.me", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3"))
x, _ := c.createRecords(zoneNameIDMapper, exclude)
if assert.NotNil(t, x) {
assert.Equal(t, endpoints, x)
}
err = c.createRecordsets(zoneNameIDMapper, exclude)
assert.Nil(t, err)
}
// TestDeleteRecords validate delete
func TestDeleteRecords(t *testing.T) {
config := AkamaiConfig{}
client := &mockAkamaiClient{}
c := NewAkamaiProvider(config)
c.client = client
stub := newStub()
domfilter := endpoint.DomainFilter{}
idfilter := provider.ZoneIDFilter{}
c, err := createAkamaiStubProvider(stub, domfilter, idfilter)
assert.Nil(t, err)
zoneNameIDMapper := provider.ZoneIDName{"example.com": "example.com"}
endpoints := make([]*endpoint.Endpoint, 0)
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3"))
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"))
x, _ := c.deleteRecords(zoneNameIDMapper, endpoints)
if assert.NotNil(t, x) {
assert.Equal(t, endpoints, x)
}
err = c.deleteRecordsets(zoneNameIDMapper, endpoints)
assert.Nil(t, err)
}
//
func TestDeleteRecordsDomainFilter(t *testing.T) {
config := AkamaiConfig{
DomainFilter: endpoint.NewDomainFilter([]string{"example.com"}),
}
client := &mockAkamaiClient{}
c := NewAkamaiProvider(config)
c.client = client
stub := newStub()
domfilter := endpoint.NewDomainFilter([]string{"example.com"})
idfilter := provider.ZoneIDFilter{}
c, err := createAkamaiStubProvider(stub, domfilter, idfilter)
assert.Nil(t, err)
zoneNameIDMapper := provider.ZoneIDName{"example.com": "example.com"}
endpoints := make([]*endpoint.Endpoint, 0)
@ -274,38 +342,36 @@ func TestDeleteRecordsDomainFilter(t *testing.T) {
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"))
exclude := append(endpoints, endpoint.NewEndpoint("www.exclude.me", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3"))
x, _ := c.deleteRecords(zoneNameIDMapper, exclude)
if assert.NotNil(t, x) {
assert.Equal(t, endpoints, x)
}
err = c.deleteRecordsets(zoneNameIDMapper, exclude)
assert.Nil(t, err)
}
// Test record update func
func TestUpdateRecords(t *testing.T) {
config := AkamaiConfig{}
client := &mockAkamaiClient{}
c := NewAkamaiProvider(config)
c.client = client
stub := newStub()
domfilter := endpoint.DomainFilter{}
idfilter := provider.ZoneIDFilter{}
c, err := createAkamaiStubProvider(stub, domfilter, idfilter)
assert.Nil(t, err)
zoneNameIDMapper := provider.ZoneIDName{"example.com": "example.com"}
endpoints := make([]*endpoint.Endpoint, 0)
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3"))
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"))
x, _ := c.updateNewRecords(zoneNameIDMapper, endpoints)
if assert.NotNil(t, x) {
assert.Equal(t, endpoints, x)
}
err = c.updateNewRecordsets(zoneNameIDMapper, endpoints)
assert.Nil(t, err)
}
//
func TestUpdateRecordsDomainFilter(t *testing.T) {
config := AkamaiConfig{
DomainFilter: endpoint.NewDomainFilter([]string{"example.com"}),
}
client := &mockAkamaiClient{}
c := NewAkamaiProvider(config)
c.client = client
stub := newStub()
domfilter := endpoint.NewDomainFilter([]string{"example.com"})
idfilter := provider.ZoneIDFilter{}
c, err := createAkamaiStubProvider(stub, domfilter, idfilter)
assert.Nil(t, err)
zoneNameIDMapper := provider.ZoneIDName{"example.com": "example.com"}
endpoints := make([]*endpoint.Endpoint, 0)
@ -313,19 +379,19 @@ func TestUpdateRecordsDomainFilter(t *testing.T) {
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"))
exclude := append(endpoints, endpoint.NewEndpoint("www.exclude.me", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3"))
x, _ := c.updateNewRecords(zoneNameIDMapper, exclude)
if assert.NotNil(t, x) {
assert.Equal(t, endpoints, x)
}
err = c.updateNewRecordsets(zoneNameIDMapper, exclude)
assert.Nil(t, err)
}
func TestAkamaiApplyChanges(t *testing.T) {
config := AkamaiConfig{}
client := &mockAkamaiClient{}
c := NewAkamaiProvider(config)
c.client = client
stub := newStub()
domfilter := endpoint.NewDomainFilter([]string{"example.com"})
idfilter := provider.ZoneIDFilter{}
c, err := createAkamaiStubProvider(stub, domfilter, idfilter)
assert.Nil(t, err)
stub.setOutput("zone", []interface{}{"example.com"})
changes := &plan.Changes{}
changes.Create = []*endpoint.Endpoint{
{DNSName: "www.example.com", RecordType: "A", Targets: endpoint.Targets{"target"}, RecordTTL: 300},

View File

@ -74,6 +74,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 +99,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",
@ -205,6 +207,13 @@ func NewAWSProvider(awsConfig AWSConfig) (*AWSProvider, error) {
return provider, nil
}
func (p *AWSProvider) PropertyValuesEqual(name string, previous string, current string) bool {
if name == "aws/evaluate-target-health" {
return true
}
return p.BaseProvider.PropertyValuesEqual(name, previous, current)
}
// Zones returns the list of hosted zones.
func (p *AWSProvider) Zones(ctx context.Context) (map[string]*route53.HostedZone, error) {
if p.zonesCache.zones != nil && time.Since(p.zonesCache.age) < p.zonesCache.duration {
@ -382,11 +391,6 @@ func (p *AWSProvider) CreateRecords(ctx context.Context, endpoints []*endpoint.E
return p.doRecords(ctx, route53.ChangeActionCreate, endpoints)
}
// UpdateRecords updates a given set of old records to a new set of records in a given hosted zone.
func (p *AWSProvider) UpdateRecords(ctx context.Context, endpoints, _ []*endpoint.Endpoint) error {
return p.doRecords(ctx, route53.ChangeActionUpsert, endpoints)
}
// DeleteRecords deletes a given set of DNS records in a given zone.
func (p *AWSProvider) DeleteRecords(ctx context.Context, endpoints []*endpoint.Endpoint) error {
return p.doRecords(ctx, route53.ChangeActionDelete, endpoints)
@ -405,6 +409,47 @@ func (p *AWSProvider) doRecords(ctx context.Context, action string, endpoints []
return p.submitChanges(ctx, p.newChanges(action, endpoints, records, zones), zones)
}
// UpdateRecords updates a given set of old records to a new set of records in a given hosted zone.
func (p *AWSProvider) UpdateRecords(ctx context.Context, updates, current []*endpoint.Endpoint) error {
zones, err := p.Zones(ctx)
if err != nil {
return errors.Wrapf(err, "failed to list zones, aborting UpdateRecords")
}
records, err := p.records(ctx, zones)
if err != nil {
log.Errorf("failed to list records while preparing UpdateRecords: %s", err)
}
return p.submitChanges(ctx, p.createUpdateChanges(updates, current, records, zones), zones)
}
func (p *AWSProvider) createUpdateChanges(newEndpoints, oldEndpoints []*endpoint.Endpoint, recordsCache []*endpoint.Endpoint, zones map[string]*route53.HostedZone) []*route53.Change {
var deletes []*endpoint.Endpoint
var creates []*endpoint.Endpoint
var updates []*endpoint.Endpoint
for i, new := range newEndpoints {
old := oldEndpoints[i]
if new.RecordType != old.RecordType ||
// Handle the case where an AWS ALIAS record is changing to/from a CNAME.
(old.RecordType == endpoint.RecordTypeCNAME && useAlias(old, p.preferCNAME) != useAlias(new, p.preferCNAME)) {
// The record type changed, so UPSERT will fail. Instead perform a DELETE followed by a CREATE.
deletes = append(deletes, old)
creates = append(creates, new)
} else {
// Safe to perform an UPSERT.
updates = append(updates, new)
}
}
combined := make([]*route53.Change, 0, len(deletes)+len(creates)+len(updates))
combined = append(combined, p.newChanges(route53.ChangeActionCreate, creates, recordsCache, zones)...)
combined = append(combined, p.newChanges(route53.ChangeActionUpsert, updates, recordsCache, zones)...)
combined = append(combined, p.newChanges(route53.ChangeActionDelete, deletes, recordsCache, zones)...)
return combined
}
// 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)
@ -421,11 +466,12 @@ func (p *AWSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) e
}
}
combinedChanges := make([]*route53.Change, 0, len(changes.Create)+len(changes.UpdateNew)+len(changes.Delete))
updateChanges := p.createUpdateChanges(changes.UpdateNew, changes.UpdateOld, records, zones)
combinedChanges := make([]*route53.Change, 0, len(changes.Delete)+len(changes.Create)+len(updateChanges))
combinedChanges = append(combinedChanges, p.newChanges(route53.ChangeActionCreate, changes.Create, records, zones)...)
combinedChanges = append(combinedChanges, p.newChanges(route53.ChangeActionUpsert, changes.UpdateNew, records, zones)...)
combinedChanges = append(combinedChanges, p.newChanges(route53.ChangeActionDelete, changes.Delete, records, zones)...)
combinedChanges = append(combinedChanges, updateChanges...)
return p.submitChanges(ctx, combinedChanges, zones)
}

View File

@ -384,6 +384,7 @@ func TestAWSUpdateRecords(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{
endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"),
endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.4.4"),
endpoint.NewEndpointWithTTL("update-test-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-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.elb.amazonaws.com"),
endpoint.NewEndpointWithTTL("create-test-multiple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8", "8.8.4.4"),
})
@ -391,12 +392,14 @@ func TestAWSUpdateRecords(t *testing.T) {
currentRecords := []*endpoint.Endpoint{
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-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"),
endpoint.NewEndpoint("create-test-multiple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8", "8.8.4.4"),
}
updatedRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", 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-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "bar.elb.amazonaws.com"),
endpoint.NewEndpoint("create-test-multiple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4", "4.3.2.1"),
}
@ -409,6 +412,7 @@ func TestAWSUpdateRecords(t *testing.T) {
validateEndpoints(t, records, []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4"),
endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "4.3.2.1"),
endpoint.NewEndpointWithTTL("update-test-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-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "bar.elb.amazonaws.com"),
endpoint.NewEndpointWithTTL("create-test-multiple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4", "4.3.2.1"),
})
@ -456,6 +460,8 @@ func TestAWSApplyChanges(t *testing.T) {
endpoint.NewEndpointWithTTL("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"),
endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.4.4"),
endpoint.NewEndpointWithTTL("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.4.4"),
endpoint.NewEndpointWithTTL("update-test-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-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"),
@ -475,6 +481,8 @@ func TestAWSApplyChanges(t *testing.T) {
currentRecords := []*endpoint.Endpoint{
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-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"),
@ -482,6 +490,8 @@ 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-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"),
endpoint.NewEndpoint("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4", "4.3.2.1"),
@ -520,6 +530,8 @@ 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-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"),
endpoint.NewEndpointWithTTL("create-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.elb.amazonaws.com"),
@ -536,6 +548,7 @@ func TestAWSApplyChangesDryRun(t *testing.T) {
endpoint.NewEndpointWithTTL("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"),
endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.4.4"),
endpoint.NewEndpointWithTTL("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.4.4"),
endpoint.NewEndpointWithTTL("update-test-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-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"),
@ -557,6 +570,7 @@ func TestAWSApplyChangesDryRun(t *testing.T) {
currentRecords := []*endpoint.Endpoint{
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-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"),
@ -564,6 +578,7 @@ func TestAWSApplyChangesDryRun(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-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "baz.elb.amazonaws.com"),
endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "baz.elb.amazonaws.com"),
endpoint.NewEndpoint("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4", "4.3.2.1"),
@ -1100,6 +1115,51 @@ func TestAWSSuitableZones(t *testing.T) {
}
}
func TestAWSHealthTargetAnnotation(tt *testing.T) {
comparator := func(name, previous, current string) bool {
return previous == current
}
for _, test := range []struct {
name string
current *endpoint.Endpoint
desired *endpoint.Endpoint
propertyComparator func(name, previous, current string) bool
shouldUpdate bool
}{
{
name: "skip AWS target health",
current: &endpoint.Endpoint{
RecordType: "A",
DNSName: "foo.com",
ProviderSpecific: []endpoint.ProviderSpecificProperty{
{Name: "aws/evaluate-target-health", Value: "true"},
},
},
desired: &endpoint.Endpoint{
DNSName: "foo.com",
RecordType: "A",
ProviderSpecific: []endpoint.ProviderSpecificProperty{
{Name: "aws/evaluate-target-health", Value: "false"},
},
},
propertyComparator: comparator,
shouldUpdate: false,
},
} {
tt.Run(test.name, func(t *testing.T) {
provider := &AWSProvider{}
plan := &plan.Plan{
Current: []*endpoint.Endpoint{test.current},
Desired: []*endpoint.Endpoint{test.desired},
PropertyComparator: provider.PropertyValuesEqual,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
plan = plan.Calculate()
assert.Equal(t, test.shouldUpdate, len(plan.Changes.UpdateNew) == 1)
})
}
}
func createAWSZone(t *testing.T, provider *AWSProvider, zone *route53.HostedZone) {
params := &route53.CreateHostedZoneInput{
CallerReference: aws.String("external-dns.alpha.kubernetes.io/test-zone"),

View File

@ -19,17 +19,12 @@ package azure
import (
"context"
"fmt"
"io/ioutil"
"strings"
log "github.com/sirupsen/logrus"
yaml "gopkg.in/yaml.v2"
"github.com/Azure/azure-sdk-for-go/services/dns/mgmt/2018-05-01/dns"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/adal"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/Azure/go-autorest/autorest/to"
"sigs.k8s.io/external-dns/endpoint"
@ -41,18 +36,6 @@ const (
azureRecordTTL = 300
)
type config struct {
Cloud string `json:"cloud" yaml:"cloud"`
TenantID string `json:"tenantId" yaml:"tenantId"`
SubscriptionID string `json:"subscriptionId" yaml:"subscriptionId"`
ResourceGroup string `json:"resourceGroup" yaml:"resourceGroup"`
Location string `json:"location" yaml:"location"`
ClientID string `json:"aadClientId" yaml:"aadClientId"`
ClientSecret string `json:"aadClientSecret" yaml:"aadClientSecret"`
UseManagedIdentityExtension bool `json:"useManagedIdentityExtension" yaml:"useManagedIdentityExtension"`
UserAssignedIdentityID string `json:"userAssignedIdentityID" yaml:"userAssignedIdentityID"`
}
// ZonesClient is an interface of dns.ZoneClient that can be stubbed for testing.
type ZonesClient interface {
ListByResourceGroupComplete(ctx context.Context, resourceGroupName string, top *int32) (result dns.ZoneListResultIterator, err error)
@ -82,46 +65,22 @@ type AzureProvider struct {
//
// Returns the provider or an error if a provider could not be created.
func NewAzureProvider(configFile string, domainFilter endpoint.DomainFilter, zoneNameFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, resourceGroup string, userAssignedIdentityClientID string, dryRun bool) (*AzureProvider, error) {
contents, err := ioutil.ReadFile(configFile)
if err != nil {
return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err)
}
cfg := config{}
err = yaml.Unmarshal(contents, &cfg)
cfg, err := getConfig(configFile, resourceGroup, userAssignedIdentityClientID)
if err != nil {
return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err)
}
// If a resource group was given, override what was present in the config file
if resourceGroup != "" {
cfg.ResourceGroup = resourceGroup
}
// If userAssignedIdentityClientID is provided explicitly, override existing one in config file
if userAssignedIdentityClientID != "" {
cfg.UserAssignedIdentityID = userAssignedIdentityClientID
}
var environment azure.Environment
if cfg.Cloud == "" {
environment = azure.PublicCloud
} else {
environment, err = azure.EnvironmentFromName(cfg.Cloud)
if err != nil {
return nil, fmt.Errorf("invalid cloud value '%s': %v", cfg.Cloud, err)
}
}
token, err := getAccessToken(cfg, environment)
token, err := getAccessToken(*cfg, cfg.Environment)
if err != nil {
return nil, fmt.Errorf("failed to get token: %v", err)
}
zonesClient := dns.NewZonesClientWithBaseURI(environment.ResourceManagerEndpoint, cfg.SubscriptionID)
zonesClient := dns.NewZonesClientWithBaseURI(cfg.Environment.ResourceManagerEndpoint, cfg.SubscriptionID)
zonesClient.Authorizer = autorest.NewBearerAuthorizer(token)
recordSetsClient := dns.NewRecordSetsClientWithBaseURI(environment.ResourceManagerEndpoint, cfg.SubscriptionID)
recordSetsClient := dns.NewRecordSetsClientWithBaseURI(cfg.Environment.ResourceManagerEndpoint, cfg.SubscriptionID)
recordSetsClient.Authorizer = autorest.NewBearerAuthorizer(token)
provider := &AzureProvider{
return &AzureProvider{
domainFilter: domainFilter,
zoneNameFilter: zoneNameFilter,
zoneIDFilter: zoneIDFilter,
@ -130,61 +89,7 @@ func NewAzureProvider(configFile string, domainFilter endpoint.DomainFilter, zon
userAssignedIdentityClientID: cfg.UserAssignedIdentityID,
zonesClient: zonesClient,
recordSetsClient: recordSetsClient,
}
return provider, nil
}
// getAccessToken retrieves Azure API access token.
func getAccessToken(cfg config, environment azure.Environment) (*adal.ServicePrincipalToken, error) {
// Try to retrieve token with service principal credentials.
// Try to use service principal first, some AKS clusters are in an intermediate state that `UseManagedIdentityExtension` is `true`
// and service principal exists. In this case, we still want to use service principal to authenticate.
if len(cfg.ClientID) > 0 &&
len(cfg.ClientSecret) > 0 &&
// due to some historical reason, for pure MSI cluster,
// they will use "msi" as placeholder in azure.json.
// In this case, we shouldn't try to use SPN to authenticate.
!strings.EqualFold(cfg.ClientID, "msi") &&
!strings.EqualFold(cfg.ClientSecret, "msi") {
log.Info("Using client_id+client_secret to retrieve access token for Azure API.")
oauthConfig, err := adal.NewOAuthConfig(environment.ActiveDirectoryEndpoint, cfg.TenantID)
if err != nil {
return nil, fmt.Errorf("failed to retrieve OAuth config: %v", err)
}
token, err := adal.NewServicePrincipalToken(*oauthConfig, cfg.ClientID, cfg.ClientSecret, environment.ResourceManagerEndpoint)
if err != nil {
return nil, fmt.Errorf("failed to create service principal token: %v", err)
}
return token, nil
}
// Try to retrieve token with MSI.
if cfg.UseManagedIdentityExtension {
log.Info("Using managed identity extension to retrieve access token for Azure API.")
msiEndpoint, err := adal.GetMSIVMEndpoint()
if err != nil {
return nil, fmt.Errorf("failed to get the managed service identity endpoint: %v", err)
}
if cfg.UserAssignedIdentityID != "" {
log.Infof("Resolving to user assigned identity, client id is %s.", cfg.UserAssignedIdentityID)
token, err := adal.NewServicePrincipalTokenFromMSIWithUserAssignedID(msiEndpoint, environment.ServiceManagementEndpoint, cfg.UserAssignedIdentityID)
if err != nil {
return nil, fmt.Errorf("failed to create the managed service identity token: %v", err)
}
return token, nil
}
log.Info("Resolving to system assigned identity.")
token, err := adal.NewServicePrincipalTokenFromMSI(msiEndpoint, environment.ServiceManagementEndpoint)
if err != nil {
return nil, fmt.Errorf("failed to create the managed service identity token: %v", err)
}
return token, nil
}
return nil, fmt.Errorf("no credentials provided for Azure API")
}, nil
}
// Records gets the current records.
@ -352,20 +257,20 @@ func (p *AzureProvider) mapChanges(zones []dns.Zone, changes *plan.Changes) (azu
func (p *AzureProvider) deleteRecords(ctx context.Context, deleted azureChangeMap) {
// Delete records first
for zone, endpoints := range deleted {
for _, endpoint := range endpoints {
name := p.recordSetNameForZone(zone, endpoint)
if !p.domainFilter.Match(endpoint.DNSName) {
log.Debugf("Skipping deletion of record %s because it was filtered out by the specified --domain-filter", endpoint.DNSName)
for _, ep := range endpoints {
name := p.recordSetNameForZone(zone, ep)
if !p.domainFilter.Match(ep.DNSName) {
log.Debugf("Skipping deletion of record %s because it was filtered out by the specified --domain-filter", ep.DNSName)
continue
}
if p.dryRun {
log.Infof("Would delete %s record named '%s' for Azure DNS zone '%s'.", endpoint.RecordType, name, zone)
log.Infof("Would delete %s record named '%s' for Azure DNS zone '%s'.", ep.RecordType, name, zone)
} else {
log.Infof("Deleting %s record named '%s' for Azure DNS zone '%s'.", endpoint.RecordType, name, zone)
if _, err := p.recordSetsClient.Delete(ctx, p.resourceGroup, zone, name, dns.RecordType(endpoint.RecordType), ""); err != nil {
log.Infof("Deleting %s record named '%s' for Azure DNS zone '%s'.", ep.RecordType, name, zone)
if _, err := p.recordSetsClient.Delete(ctx, p.resourceGroup, zone, name, dns.RecordType(ep.RecordType), ""); err != nil {
log.Errorf(
"Failed to delete %s record named '%s' for Azure DNS zone '%s': %v",
endpoint.RecordType,
ep.RecordType,
name,
zone,
err,
@ -378,18 +283,18 @@ func (p *AzureProvider) deleteRecords(ctx context.Context, deleted azureChangeMa
func (p *AzureProvider) updateRecords(ctx context.Context, updated azureChangeMap) {
for zone, endpoints := range updated {
for _, endpoint := range endpoints {
name := p.recordSetNameForZone(zone, endpoint)
if !p.domainFilter.Match(endpoint.DNSName) {
log.Debugf("Skipping update of record %s because it was filtered out by the specified --domain-filter", endpoint.DNSName)
for _, ep := range endpoints {
name := p.recordSetNameForZone(zone, ep)
if !p.domainFilter.Match(ep.DNSName) {
log.Debugf("Skipping update of record %s because it was filtered out by the specified --domain-filter", ep.DNSName)
continue
}
if p.dryRun {
log.Infof(
"Would update %s record named '%s' to '%s' for Azure DNS zone '%s'.",
endpoint.RecordType,
ep.RecordType,
name,
endpoint.Targets,
ep.Targets,
zone,
)
continue
@ -397,20 +302,20 @@ func (p *AzureProvider) updateRecords(ctx context.Context, updated azureChangeMa
log.Infof(
"Updating %s record named '%s' to '%s' for Azure DNS zone '%s'.",
endpoint.RecordType,
ep.RecordType,
name,
endpoint.Targets,
ep.Targets,
zone,
)
recordSet, err := p.newRecordSet(endpoint)
recordSet, err := p.newRecordSet(ep)
if err == nil {
_, err = p.recordSetsClient.CreateOrUpdate(
ctx,
p.resourceGroup,
zone,
name,
dns.RecordType(endpoint.RecordType),
dns.RecordType(ep.RecordType),
recordSet,
"",
"",
@ -419,9 +324,9 @@ func (p *AzureProvider) updateRecords(ctx context.Context, updated azureChangeMa
if err != nil {
log.Errorf(
"Failed to update %s record named '%s' to '%s' for DNS zone '%s': %v",
endpoint.RecordType,
ep.RecordType,
name,
endpoint.Targets,
ep.Targets,
zone,
err,
)

View File

@ -21,9 +21,8 @@ import (
"fmt"
"strings"
"github.com/Azure/azure-sdk-for-go/profiles/latest/privatedns/mgmt/privatedns"
"github.com/Azure/azure-sdk-for-go/services/privatedns/mgmt/2018-09-01/privatedns"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/azure/auth"
"github.com/Azure/go-autorest/autorest/to"
log "github.com/sirupsen/logrus"
@ -47,44 +46,43 @@ type PrivateRecordSetsClient interface {
// AzurePrivateDNSProvider implements the DNS provider for Microsoft's Azure Private DNS service
type AzurePrivateDNSProvider struct {
provider.BaseProvider
domainFilter endpoint.DomainFilter
zoneIDFilter provider.ZoneIDFilter
dryRun bool
subscriptionID string
resourceGroup string
zonesClient PrivateZonesClient
recordSetsClient PrivateRecordSetsClient
domainFilter endpoint.DomainFilter
zoneIDFilter provider.ZoneIDFilter
dryRun bool
resourceGroup string
userAssignedIdentityClientID string
zonesClient PrivateZonesClient
recordSetsClient PrivateRecordSetsClient
}
// NewAzurePrivateDNSProvider creates a new Azure Private DNS provider.
//
// Returns the provider or an error if a provider could not be created.
func NewAzurePrivateDNSProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, resourceGroup string, subscriptionID string, dryRun bool) (*AzurePrivateDNSProvider, error) {
authorizer, err := auth.NewAuthorizerFromEnvironment()
func NewAzurePrivateDNSProvider(configFile string, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, resourceGroup, userAssignedIdentityClientID string, dryRun bool) (*AzurePrivateDNSProvider, error) {
cfg, err := getConfig(configFile, resourceGroup, userAssignedIdentityClientID)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err)
}
settings, err := auth.GetSettingsFromEnvironment()
token, err := getAccessToken(*cfg, cfg.Environment)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to get token: %v", err)
}
zonesClient := privatedns.NewPrivateZonesClientWithBaseURI(settings.Environment.ResourceManagerEndpoint, subscriptionID)
zonesClient.Authorizer = authorizer
recordSetsClient := privatedns.NewRecordSetsClientWithBaseURI(settings.Environment.ResourceManagerEndpoint, subscriptionID)
recordSetsClient.Authorizer = authorizer
zonesClient := privatedns.NewPrivateZonesClientWithBaseURI(cfg.Environment.ResourceManagerEndpoint, cfg.SubscriptionID)
zonesClient.Authorizer = autorest.NewBearerAuthorizer(token)
recordSetsClient := privatedns.NewRecordSetsClientWithBaseURI(cfg.Environment.ResourceManagerEndpoint, cfg.SubscriptionID)
recordSetsClient.Authorizer = autorest.NewBearerAuthorizer(token)
provider := &AzurePrivateDNSProvider{
domainFilter: domainFilter,
zoneIDFilter: zoneIDFilter,
dryRun: dryRun,
subscriptionID: subscriptionID,
resourceGroup: resourceGroup,
zonesClient: zonesClient,
recordSetsClient: recordSetsClient,
}
return provider, nil
return &AzurePrivateDNSProvider{
domainFilter: domainFilter,
zoneIDFilter: zoneIDFilter,
dryRun: dryRun,
resourceGroup: cfg.ResourceGroup,
userAssignedIdentityClientID: cfg.UserAssignedIdentityID,
zonesClient: zonesClient,
recordSetsClient: recordSetsClient,
}, nil
}
// Records gets the current records.
@ -256,16 +254,16 @@ func (p *AzurePrivateDNSProvider) deleteRecords(ctx context.Context, deleted azu
log.Debugf("Records to be deleted: %d", len(deleted))
// Delete records first
for zone, endpoints := range deleted {
for _, endpoint := range endpoints {
name := p.recordSetNameForZone(zone, endpoint)
for _, ep := range endpoints {
name := p.recordSetNameForZone(zone, ep)
if p.dryRun {
log.Infof("Would delete %s record named '%s' for Azure Private DNS zone '%s'.", endpoint.RecordType, name, zone)
log.Infof("Would delete %s record named '%s' for Azure Private DNS zone '%s'.", ep.RecordType, name, zone)
} else {
log.Infof("Deleting %s record named '%s' for Azure Private DNS zone '%s'.", endpoint.RecordType, name, zone)
if _, err := p.recordSetsClient.Delete(ctx, p.resourceGroup, zone, privatedns.RecordType(endpoint.RecordType), name, ""); err != nil {
log.Infof("Deleting %s record named '%s' for Azure Private DNS zone '%s'.", ep.RecordType, name, zone)
if _, err := p.recordSetsClient.Delete(ctx, p.resourceGroup, zone, privatedns.RecordType(ep.RecordType), name, ""); err != nil {
log.Errorf(
"Failed to delete %s record named '%s' for Azure Private DNS zone '%s': %v",
endpoint.RecordType,
ep.RecordType,
name,
zone,
err,
@ -279,14 +277,14 @@ func (p *AzurePrivateDNSProvider) deleteRecords(ctx context.Context, deleted azu
func (p *AzurePrivateDNSProvider) updateRecords(ctx context.Context, updated azurePrivateDNSChangeMap) {
log.Debugf("Records to be updated: %d", len(updated))
for zone, endpoints := range updated {
for _, endpoint := range endpoints {
name := p.recordSetNameForZone(zone, endpoint)
for _, ep := range endpoints {
name := p.recordSetNameForZone(zone, ep)
if p.dryRun {
log.Infof(
"Would update %s record named '%s' to '%s' for Azure Private DNS zone '%s'.",
endpoint.RecordType,
ep.RecordType,
name,
endpoint.Targets,
ep.Targets,
zone,
)
continue
@ -294,19 +292,19 @@ func (p *AzurePrivateDNSProvider) updateRecords(ctx context.Context, updated azu
log.Infof(
"Updating %s record named '%s' to '%s' for Azure Private DNS zone '%s'.",
endpoint.RecordType,
ep.RecordType,
name,
endpoint.Targets,
ep.Targets,
zone,
)
recordSet, err := p.newRecordSet(endpoint)
recordSet, err := p.newRecordSet(ep)
if err == nil {
_, err = p.recordSetsClient.CreateOrUpdate(
ctx,
p.resourceGroup,
zone,
privatedns.RecordType(endpoint.RecordType),
privatedns.RecordType(ep.RecordType),
name,
recordSet,
"",
@ -316,9 +314,9 @@ func (p *AzurePrivateDNSProvider) updateRecords(ctx context.Context, updated azu
if err != nil {
log.Errorf(
"Failed to update %s record named '%s' to '%s' for Azure Private DNS zone '%s': %v",
endpoint.RecordType,
ep.RecordType,
name,
endpoint.Targets,
ep.Targets,
zone,
err,
)

View File

@ -18,16 +18,11 @@ package azure
import (
"context"
"os"
"testing"
"github.com/Azure/azure-sdk-for-go/services/privatedns/mgmt/2018-09-01/privatedns"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/Azure/go-autorest/autorest/azure/auth"
"github.com/Azure/go-autorest/autorest/to"
"github.com/stretchr/testify/assert"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
@ -255,36 +250,6 @@ func newAzurePrivateDNSProvider(domainFilter endpoint.DomainFilter, zoneIDFilter
}
}
func validateAzurePrivateDNSClientsResourceManager(t *testing.T, environmentName string, expectedResourceManagerEndpoint string) {
err := os.Setenv(auth.EnvironmentName, environmentName)
if err != nil {
t.Fatal(err)
}
azurePrivateDNSProvider, err := NewAzurePrivateDNSProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{""}), "k8s", "sub", true)
if err != nil {
t.Fatal(err)
}
zonesClientBaseURI := azurePrivateDNSProvider.zonesClient.(privatedns.PrivateZonesClient).BaseURI
recordSetsClientBaseURI := azurePrivateDNSProvider.recordSetsClient.(privatedns.RecordSetsClient).BaseURI
assert.Equal(t, zonesClientBaseURI, expectedResourceManagerEndpoint, "expected and actual resource manager endpoints don't match. expected: %s, got: %s", expectedResourceManagerEndpoint, zonesClientBaseURI)
assert.Equal(t, recordSetsClientBaseURI, expectedResourceManagerEndpoint, "expected and actual resource manager endpoints don't match. expected: %s, got: %s", expectedResourceManagerEndpoint, recordSetsClientBaseURI)
}
func TestNewAzurePrivateDNSProvider(t *testing.T) {
// make sure to reset the environment variables at the end again
originalEnv := os.Getenv(auth.EnvironmentName)
defer os.Setenv(auth.EnvironmentName, originalEnv)
validateAzurePrivateDNSClientsResourceManager(t, "", azure.PublicCloud.ResourceManagerEndpoint)
validateAzurePrivateDNSClientsResourceManager(t, "AZURECHINACLOUD", azure.ChinaCloud.ResourceManagerEndpoint)
validateAzurePrivateDNSClientsResourceManager(t, "AZUREGERMANCLOUD", azure.GermanCloud.ResourceManagerEndpoint)
validateAzurePrivateDNSClientsResourceManager(t, "AZUREUSGOVERNMENTCLOUD", azure.USGovernmentCloud.ResourceManagerEndpoint)
}
func TestAzurePrivateDNSRecord(t *testing.T) {
provider, err := newMockedAzurePrivateDNSProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{""}), true, "k8s",
&[]privatedns.PrivateZone{

129
provider/azure/config.go Normal file
View File

@ -0,0 +1,129 @@
/*
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 azure
import (
"fmt"
"io/ioutil"
"strings"
"github.com/Azure/go-autorest/autorest/adal"
"github.com/Azure/go-autorest/autorest/azure"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v2"
)
// config represents common config items for Azure DNS and Azure Private DNS
type config struct {
Cloud string `json:"cloud" yaml:"cloud"`
Environment azure.Environment `json:"-" yaml:"-"`
TenantID string `json:"tenantId" yaml:"tenantId"`
SubscriptionID string `json:"subscriptionId" yaml:"subscriptionId"`
ResourceGroup string `json:"resourceGroup" yaml:"resourceGroup"`
Location string `json:"location" yaml:"location"`
ClientID string `json:"aadClientId" yaml:"aadClientId"`
ClientSecret string `json:"aadClientSecret" yaml:"aadClientSecret"`
UseManagedIdentityExtension bool `json:"useManagedIdentityExtension" yaml:"useManagedIdentityExtension"`
UserAssignedIdentityID string `json:"userAssignedIdentityID" yaml:"userAssignedIdentityID"`
}
func getConfig(configFile, resourceGroup, userAssignedIdentityClientID string) (*config, error) {
contents, err := ioutil.ReadFile(configFile)
if err != nil {
return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err)
}
cfg := &config{}
err = yaml.Unmarshal(contents, &cfg)
if err != nil {
return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err)
}
// If a resource group was given, override what was present in the config file
if resourceGroup != "" {
cfg.ResourceGroup = resourceGroup
}
// If userAssignedIdentityClientID is provided explicitly, override existing one in config file
if userAssignedIdentityClientID != "" {
cfg.UserAssignedIdentityID = userAssignedIdentityClientID
}
var environment azure.Environment
if cfg.Cloud == "" {
environment = azure.PublicCloud
} else {
environment, err = azure.EnvironmentFromName(cfg.Cloud)
if err != nil {
return nil, fmt.Errorf("invalid cloud value '%s': %v", cfg.Cloud, err)
}
}
cfg.Environment = environment
return cfg, nil
}
// getAccessToken retrieves Azure API access token.
func getAccessToken(cfg config, environment azure.Environment) (*adal.ServicePrincipalToken, error) {
// Try to retrieve token with service principal credentials.
// Try to use service principal first, some AKS clusters are in an intermediate state that `UseManagedIdentityExtension` is `true`
// and service principal exists. In this case, we still want to use service principal to authenticate.
if len(cfg.ClientID) > 0 &&
len(cfg.ClientSecret) > 0 &&
// due to some historical reason, for pure MSI cluster,
// they will use "msi" as placeholder in azure.json.
// In this case, we shouldn't try to use SPN to authenticate.
!strings.EqualFold(cfg.ClientID, "msi") &&
!strings.EqualFold(cfg.ClientSecret, "msi") {
log.Info("Using client_id+client_secret to retrieve access token for Azure API.")
oauthConfig, err := adal.NewOAuthConfig(environment.ActiveDirectoryEndpoint, cfg.TenantID)
if err != nil {
return nil, fmt.Errorf("failed to retrieve OAuth config: %v", err)
}
token, err := adal.NewServicePrincipalToken(*oauthConfig, cfg.ClientID, cfg.ClientSecret, environment.ResourceManagerEndpoint)
if err != nil {
return nil, fmt.Errorf("failed to create service principal token: %v", err)
}
return token, nil
}
// Try to retrieve token with MSI.
if cfg.UseManagedIdentityExtension {
log.Info("Using managed identity extension to retrieve access token for Azure API.")
msiEndpoint, err := adal.GetMSIVMEndpoint()
if err != nil {
return nil, fmt.Errorf("failed to get the managed service identity endpoint: %v", err)
}
if cfg.UserAssignedIdentityID != "" {
log.Infof("Resolving to user assigned identity, client id is %s.", cfg.UserAssignedIdentityID)
token, err := adal.NewServicePrincipalTokenFromMSIWithUserAssignedID(msiEndpoint, environment.ServiceManagementEndpoint, cfg.UserAssignedIdentityID)
if err != nil {
return nil, fmt.Errorf("failed to create the managed service identity token: %v", err)
}
return token, nil
}
log.Info("Resolving to system assigned identity.")
token, err := adal.NewServicePrincipalTokenFromMSI(msiEndpoint, environment.ServiceManagementEndpoint)
if err != nil {
return nil, fmt.Errorf("failed to create the managed service identity token: %v", err)
}
return token, nil
}
return nil, fmt.Errorf("no credentials provided for Azure API")
}

View File

@ -0,0 +1,67 @@
/*
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 azure
import (
"fmt"
"github.com/Azure/go-autorest/autorest/azure"
"io/ioutil"
"os"
"reflect"
"testing"
)
func TestGetAzureEnvironmentConfig(t *testing.T) {
tmp, err := ioutil.TempFile("", "azureconf")
if err != nil {
t.Errorf("couldn't write temp file %v", err)
}
defer os.Remove(tmp.Name())
tests := map[string]struct {
cloud string
err error
}{
"AzureChinaCloud": {"AzureChinaCloud", nil},
"AzureGermanCloud": {"AzureGermanCloud", nil},
"AzurePublicCloud": {"", nil},
"AzureUSGovernment": {"AzureUSGovernmentCloud", nil},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
_, _ = tmp.Seek(0, 0)
_, _ = tmp.Write([]byte(fmt.Sprintf(`{"cloud": "%s"}`, test.cloud)))
got, err := getConfig(tmp.Name(), "", "")
if err != nil {
t.Errorf("got unexpected err %v", err)
}
if test.cloud == "" {
test.cloud = "AzurePublicCloud"
}
want, err := azure.EnvironmentFromName(test.cloud)
if err != nil {
t.Errorf("couldn't get azure environment from provided name %v", err)
}
if !reflect.DeepEqual(want, got.Environment) {
t.Errorf("got %v, want %v", got.Environment, want)
}
})
}
}

View File

@ -157,7 +157,7 @@ func (p *CloudFlareProvider) Zones(ctx context.Context) ([]cloudflare.Zone, erro
p.PaginationOptions.Page = 1
// if there is a zoneIDfilter configured
// && if the filter isnt just a blank string (used in tests)
// && if the filter isn't just a blank string (used in tests)
if len(p.zoneIDFilter.ZoneIDs) > 0 && p.zoneIDFilter.ZoneIDs[0] != "" {
log.Debugln("zoneIDFilter configured. only looking up zone IDs defined")
for _, zoneID := range p.zoneIDFilter.ZoneIDs {
@ -331,6 +331,18 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
return nil
}
// AdjustEndpoints modifies the endpoints as needed by the specific provider
func (p *CloudFlareProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) []*endpoint.Endpoint {
adjustedEndpoints := []*endpoint.Endpoint{}
for _, e := range endpoints {
if shouldBeProxied(e, p.proxiedByDefault) {
e.RecordTTL = 0
}
adjustedEndpoints = append(adjustedEndpoints, e)
}
return adjustedEndpoints
}
// changesByZone separates a multi-zone change into a single change per zone.
func (p *CloudFlareProvider) changesByZone(zones []cloudflare.Zone, changeSet []*cloudFlareChange) map[string][]*cloudFlareChange {
changes := make(map[string][]*cloudFlareChange)

View File

@ -229,7 +229,7 @@ func (m *mockCloudFlareClient) ZoneDetails(zoneID string) (cloudflare.Zone, erro
return cloudflare.Zone{}, errors.New("Unknown zoneID: " + zoneID)
}
func AssertActions(t *testing.T, provider *CloudFlareProvider, endpoints []*endpoint.Endpoint, actions []MockAction, args ...interface{}) {
func AssertActions(t *testing.T, provider *CloudFlareProvider, endpoints []*endpoint.Endpoint, actions []MockAction, managedRecords []string, args ...interface{}) {
t.Helper()
var client *mockCloudFlareClient
@ -250,9 +250,10 @@ func AssertActions(t *testing.T, provider *CloudFlareProvider, endpoints []*endp
}
plan := &plan.Plan{
Current: records,
Desired: endpoints,
DomainFilter: endpoint.NewDomainFilter([]string{"bar.com"}),
Current: records,
Desired: endpoints,
DomainFilter: endpoint.NewDomainFilter([]string{"bar.com"}),
ManagedRecords: managedRecords,
}
changes := plan.Calculate().Changes
@ -305,7 +306,10 @@ func TestCloudflareA(t *testing.T) {
Proxied: false,
},
},
})
},
[]string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
)
}
func TestCloudflareCname(t *testing.T) {
@ -340,7 +344,9 @@ func TestCloudflareCname(t *testing.T) {
Proxied: false,
},
},
})
},
[]string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
)
}
func TestCloudflareCustomTTL(t *testing.T) {
@ -365,7 +371,9 @@ func TestCloudflareCustomTTL(t *testing.T) {
Proxied: false,
},
},
})
},
[]string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
)
}
func TestCloudflareProxiedDefault(t *testing.T) {
@ -389,7 +397,9 @@ func TestCloudflareProxiedDefault(t *testing.T) {
Proxied: true,
},
},
})
},
[]string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
)
}
func TestCloudflareProxiedOverrideTrue(t *testing.T) {
@ -419,7 +429,9 @@ func TestCloudflareProxiedOverrideTrue(t *testing.T) {
Proxied: true,
},
},
})
},
[]string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
)
}
func TestCloudflareProxiedOverrideFalse(t *testing.T) {
@ -449,7 +461,9 @@ func TestCloudflareProxiedOverrideFalse(t *testing.T) {
Proxied: false,
},
},
})
},
[]string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
)
}
func TestCloudflareProxiedOverrideIllegal(t *testing.T) {
@ -479,7 +493,9 @@ func TestCloudflareProxiedOverrideIllegal(t *testing.T) {
Proxied: true,
},
},
})
},
[]string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
)
}
func TestCloudflareSetProxied(t *testing.T) {
@ -525,7 +541,7 @@ func TestCloudflareSetProxied(t *testing.T) {
Proxied: testCase.proxiable,
},
},
}, testCase.recordType+" record on "+testCase.domain)
}, []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME, endpoint.RecordTypeNS}, testCase.recordType+" record on "+testCase.domain)
}
}
@ -1038,6 +1054,7 @@ func TestProviderPropertiesIdempotency(t *testing.T) {
Current: current,
Desired: desired,
PropertyComparator: provider.PropertyValuesEqual,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
plan = *plan.Calculate()
@ -1091,7 +1108,8 @@ func TestCloudflareComplexUpdate(t *testing.T) {
},
},
},
DomainFilter: endpoint.NewDomainFilter([]string{"bar.com"}),
DomainFilter: endpoint.NewDomainFilter([]string{"bar.com"}),
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
planned := plan.Calculate()
@ -1133,3 +1151,61 @@ func TestCloudflareComplexUpdate(t *testing.T) {
},
})
}
func TestCustomTTLWithEnabledProxyNotChanged(t *testing.T) {
client := NewMockCloudFlareClientWithRecords(map[string][]cloudflare.DNSRecord{
"001": []cloudflare.DNSRecord{
{
ID: "1234567890",
ZoneID: "001",
Name: "foobar.bar.com",
Type: endpoint.RecordTypeA,
TTL: 1,
Content: "1.2.3.4",
Proxied: true,
},
},
})
provider := &CloudFlareProvider{
Client: client,
}
records, err := provider.Records(context.Background())
if err != nil {
t.Errorf("should not fail, %s", err)
}
endpoints := []*endpoint.Endpoint{
{
DNSName: "foobar.bar.com",
Targets: endpoint.Targets{"1.2.3.4"},
RecordType: endpoint.RecordTypeA,
RecordTTL: 300,
Labels: endpoint.Labels{},
ProviderSpecific: endpoint.ProviderSpecific{
{
Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied",
Value: "true",
},
},
},
}
provider.AdjustEndpoints(endpoints)
plan := &plan.Plan{
Current: records,
Desired: endpoints,
DomainFilter: endpoint.NewDomainFilter([]string{"bar.com"}),
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
planned := plan.Calculate()
assert.Equal(t, 0, len(planned.Changes.Create), "no new changes should be here")
assert.Equal(t, 0, len(planned.Changes.UpdateNew), "no new changes should be here")
assert.Equal(t, 0, len(planned.Changes.UpdateOld), "no new changes should be here")
assert.Equal(t, 0, len(planned.Changes.Delete), "no new changes should be here")
}

View File

@ -27,6 +27,7 @@ import (
"golang.org/x/oauth2"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/pkg/apis/externaldns"
"sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
)
@ -83,7 +84,10 @@ func NewDigitalOceanProvider(ctx context.Context, domainFilter endpoint.DomainFi
oauthClient := oauth2.NewClient(ctx, oauth2.StaticTokenSource(&oauth2.Token{
AccessToken: token,
}))
client := godo.NewClient(oauthClient)
client, err := godo.New(oauthClient, godo.SetUserAgent("ExternalDNS/"+externaldns.Version))
if err != nil {
return nil, err
}
p := &DigitalOceanProvider{
Client: client.Domains,

View File

@ -195,7 +195,7 @@ func fixMissingTTL(ttl endpoint.TTL, minTTLSeconds int) string {
return strconv.Itoa(i)
}
// merge produces a singe list of records that can be used as a replacement.
// merge produces a single list of records that can be used as a replacement.
// Dyn allows to replace all records with a single call
// Invariant: the result contains only elements from the updateNew parameter
func merge(updateOld, updateNew []*endpoint.Endpoint) []*endpoint.Endpoint {
@ -625,7 +625,7 @@ func (d *dynProviderState) Records(ctx context.Context) ([]*endpoint.Endpoint, e
// this method does C + 2*Z requests: C=total number of changes, Z = number of
// affected zones (1 login + 1 commit)
func (d *dynProviderState) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
log.Debugf("Processing chages: %+v", changes)
log.Debugf("Processing changes: %+v", changes)
if d.DryRun {
log.Infof("Will NOT delete these records: %+v", changes.Delete)

300
provider/godaddy/client.go Normal file
View File

@ -0,0 +1,300 @@
/*
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
}
// 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
}

508
provider/godaddy/godaddy.go Normal file
View File

@ -0,0 +1,508 @@
/*
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"
"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 = iota
gdUpdate
gdDelete
)
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")
// ErrNoDryRun No dry run support for the moment
ErrNoDryRun = errors.New("dry run not supported")
)
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 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
}
// TODO: Add Dry Run support
if dryRun {
return nil, ErrNoDryRun
}
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) flushRecords(patch bool, zoneRecord *gdRecords) error {
if patch {
return p.client.Patch(fmt.Sprintf("/v1/domains/%s/records", zoneRecord.zone), zoneRecord.records, nil)
}
return p.client.Put(fmt.Sprintf("/v1/domains/%s/records", zoneRecord.zone), zoneRecord.records, 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(patch bool, endpoints []gdEndpoint, zoneRecords []*gdRecords) error {
zoneNameIDMapper := gdZoneIDName{}
for _, zoneRecord := range zoneRecords {
if patch {
zoneRecord.changed = false
zoneRecord.records = nil
}
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))
}
zoneRecord.applyChange(e.action, change)
}
}
}
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(gdCreate, changes.Create, allChanges)
allChanges = p.appendChange(gdCreate, changes.UpdateNew, allChanges)
allChanges = p.appendChange(gdDelete, changes.UpdateOld, allChanges)
allChanges = p.appendChange(gdDelete, changes.Delete, allChanges)
log.Infof("GoDaddy: %d changes will be done", len(allChanges))
patch := len(changes.UpdateOld)+len(changes.Delete) == 0
if err = p.changeAllRecords(patch, allChanges, changedZoneRecords); err != nil {
return err
}
for _, record := range changedZoneRecords {
if record.changed {
if err = p.flushRecords(patch, record); err != nil {
return err
}
}
}
return nil
}
func (p *gdRecords) addRecord(change gdRecordField) {
log.Debugf("GoDaddy: Add an entry %s to zone %s", change.String(), p.zone)
p.records = append(p.records, change)
p.changed = true
}
func (p *gdRecords) updateRecord(change gdRecordField) {
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 {
p.records[index] = change
p.changed = true
break
}
}
}
// Remove one record from the record list
func (p *gdRecords) deleteRecord(change gdRecordField) {
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 {
p.records[deleteIndex] = p.records[len(p.records)-1]
p.records = p.records[:len(p.records)-1]
p.changed = true
} else {
log.Warnf("GoDaddy: record in zone %s not found %s to delete", p.zone, change.String())
}
}
func (p *gdRecords) applyChange(action int, change gdRecordField) {
switch action {
case gdCreate:
p.addRecord(change)
case gdUpdate:
p.updateRecord(change)
case gdDelete:
p.deleteRecord(change)
}
}
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
}

View File

@ -0,0 +1,376 @@
/*
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"
"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()
// Update domain
client.On("Put", "/v1/domains/example.net/records", []gdRecordField{
{
Name: "@",
Type: "A",
TTL: gdMinimalTTL,
Data: "203.0.113.42",
},
}).Return(nil, nil).Once()
assert.NoError(provider.ApplyChanges(context.TODO(), &changes))
client.AssertExpectations(t)
}

View File

@ -21,6 +21,7 @@ import (
"fmt"
"net/http"
"os"
"sort"
"strconv"
"strings"
@ -157,7 +158,24 @@ func (p *InfobloxProvider) Records(ctx context.Context) (endpoints []*endpoint.E
return nil, fmt.Errorf("could not fetch A records from zone '%s': %s", zone.Fqdn, err)
}
for _, res := range resA {
endpoints = append(endpoints, endpoint.NewEndpoint(res.Name, endpoint.RecordTypeA, res.Ipv4Addr))
newEndpoint := endpoint.NewEndpoint(res.Name, endpoint.RecordTypeA, res.Ipv4Addr)
// Check if endpoint already exists and add to existing endpoint if it does
foundExisting := false
for _, ep := range endpoints {
if ep.DNSName == newEndpoint.DNSName && ep.RecordType == newEndpoint.RecordType {
logrus.Debugf("Adding target '%s' to existing A record '%s'", newEndpoint.Targets[0], ep.DNSName)
ep.Targets = append(ep.Targets, newEndpoint.Targets[0])
foundExisting = true
break
}
}
if !foundExisting {
endpoints = append(endpoints, newEndpoint)
}
}
// sort targets so that they are always in same order, as infoblox might return them in different order
for _, ep := range endpoints {
sort.Sort(ep.Targets)
}
// Include Host records since they should be treated synonymously with A records
@ -309,14 +327,14 @@ func (p *InfobloxProvider) findZone(zones []ibclient.ZoneAuth, name string) *ibc
return result
}
func (p *InfobloxProvider) recordSet(ep *endpoint.Endpoint, getObject bool) (recordSet infobloxRecordSet, err error) {
func (p *InfobloxProvider) recordSet(ep *endpoint.Endpoint, getObject bool, targetIndex int) (recordSet infobloxRecordSet, err error) {
switch ep.RecordType {
case endpoint.RecordTypeA:
var res []ibclient.RecordA
obj := ibclient.NewRecordA(
ibclient.RecordA{
Name: ep.DNSName,
Ipv4Addr: ep.Targets[0],
Ipv4Addr: ep.Targets[targetIndex],
View: p.view,
},
)
@ -399,28 +417,30 @@ func (p *InfobloxProvider) createRecords(created infobloxChangeMap) {
zone,
)
recordSet, err := p.recordSet(ep, false)
if err != nil {
logrus.Errorf(
"Failed to retrieve %s record named '%s' to '%s' for DNS zone '%s': %v",
ep.RecordType,
ep.DNSName,
ep.Targets,
zone,
err,
)
continue
}
_, err = p.client.CreateObject(recordSet.obj)
if err != nil {
logrus.Errorf(
"Failed to create %s record named '%s' to '%s' for DNS zone '%s': %v",
ep.RecordType,
ep.DNSName,
ep.Targets,
zone,
err,
)
for targetIndex := range ep.Targets {
recordSet, err := p.recordSet(ep, false, targetIndex)
if err != nil {
logrus.Errorf(
"Failed to retrieve %s record named '%s' to '%s' for DNS zone '%s': %v",
ep.RecordType,
ep.DNSName,
ep.Targets[targetIndex],
zone,
err,
)
continue
}
_, err = p.client.CreateObject(recordSet.obj)
if err != nil {
logrus.Errorf(
"Failed to create %s record named '%s' to '%s' for DNS zone '%s': %v",
ep.RecordType,
ep.DNSName,
ep.Targets[targetIndex],
zone,
err,
)
}
}
}
}
@ -434,41 +454,43 @@ func (p *InfobloxProvider) deleteRecords(deleted infobloxChangeMap) {
logrus.Infof("Would delete %s record named '%s' for Infoblox DNS zone '%s'.", ep.RecordType, ep.DNSName, zone)
} else {
logrus.Infof("Deleting %s record named '%s' for Infoblox DNS zone '%s'.", ep.RecordType, ep.DNSName, zone)
recordSet, err := p.recordSet(ep, true)
if err != nil {
logrus.Errorf(
"Failed to retrieve %s record named '%s' to '%s' for DNS zone '%s': %v",
ep.RecordType,
ep.DNSName,
ep.Targets,
zone,
err,
)
continue
}
switch ep.RecordType {
case endpoint.RecordTypeA:
for _, record := range *recordSet.res.(*[]ibclient.RecordA) {
_, err = p.client.DeleteObject(record.Ref)
for targetIndex := range ep.Targets {
recordSet, err := p.recordSet(ep, true, targetIndex)
if err != nil {
logrus.Errorf(
"Failed to retrieve %s record named '%s' to '%s' for DNS zone '%s': %v",
ep.RecordType,
ep.DNSName,
ep.Targets[targetIndex],
zone,
err,
)
continue
}
case endpoint.RecordTypeCNAME:
for _, record := range *recordSet.res.(*[]ibclient.RecordCNAME) {
_, err = p.client.DeleteObject(record.Ref)
switch ep.RecordType {
case endpoint.RecordTypeA:
for _, record := range *recordSet.res.(*[]ibclient.RecordA) {
_, err = p.client.DeleteObject(record.Ref)
}
case endpoint.RecordTypeCNAME:
for _, record := range *recordSet.res.(*[]ibclient.RecordCNAME) {
_, err = p.client.DeleteObject(record.Ref)
}
case endpoint.RecordTypeTXT:
for _, record := range *recordSet.res.(*[]ibclient.RecordTXT) {
_, err = p.client.DeleteObject(record.Ref)
}
}
case endpoint.RecordTypeTXT:
for _, record := range *recordSet.res.(*[]ibclient.RecordTXT) {
_, err = p.client.DeleteObject(record.Ref)
if err != nil {
logrus.Errorf(
"Failed to delete %s record named '%s' for Infoblox DNS zone '%s': %v",
ep.RecordType,
ep.DNSName,
zone,
err,
)
}
}
if err != nil {
logrus.Errorf(
"Failed to delete %s record named '%s' for Infoblox DNS zone '%s': %v",
ep.RecordType,
ep.DNSName,
zone,
err,
)
}
}
}
}

View File

@ -327,6 +327,18 @@ func createMockInfobloxObject(name, recordType, value string) ibclient.IBObject
Text: value,
},
)
case "HOST":
return ibclient.NewHostRecord(
ibclient.HostRecord{
Ref: ref,
Name: name,
Ipv4Addrs: []ibclient.HostRecordIpv4Addr{
{
Ipv4Addr: value,
},
},
},
)
}
return nil
}
@ -354,6 +366,13 @@ func TestInfobloxRecords(t *testing.T) {
createMockInfobloxObject("whitespace.example.com", endpoint.RecordTypeA, "123.123.123.124"),
createMockInfobloxObject("whitespace.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=white space"),
createMockInfobloxObject("hack.example.com", endpoint.RecordTypeCNAME, "cerberus.infoblox.com"),
createMockInfobloxObject("multiple.example.com", endpoint.RecordTypeA, "123.123.123.122"),
createMockInfobloxObject("multiple.example.com", endpoint.RecordTypeA, "123.123.123.121"),
createMockInfobloxObject("multiple.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"),
createMockInfobloxObject("existing.example.com", endpoint.RecordTypeA, "124.1.1.1"),
createMockInfobloxObject("existing.example.com", endpoint.RecordTypeA, "124.1.1.2"),
createMockInfobloxObject("existing.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=existing"),
createMockInfobloxObject("host.example.com", "HOST", "125.1.1.1"),
},
}
@ -371,6 +390,11 @@ func TestInfobloxRecords(t *testing.T) {
endpoint.NewEndpoint("whitespace.example.com", endpoint.RecordTypeA, "123.123.123.124"),
endpoint.NewEndpoint("whitespace.example.com", endpoint.RecordTypeTXT, "\"heritage=external-dns,external-dns/owner=white space\""),
endpoint.NewEndpoint("hack.example.com", endpoint.RecordTypeCNAME, "cerberus.infoblox.com"),
endpoint.NewEndpoint("multiple.example.com", endpoint.RecordTypeA, "123.123.123.122", "123.123.123.121"),
endpoint.NewEndpoint("multiple.example.com", endpoint.RecordTypeTXT, "\"heritage=external-dns,external-dns/owner=default\""),
endpoint.NewEndpoint("existing.example.com", endpoint.RecordTypeA, "124.1.1.1", "124.1.1.2"),
endpoint.NewEndpoint("existing.example.com", endpoint.RecordTypeTXT, "\"heritage=external-dns,external-dns/owner=existing\""),
endpoint.NewEndpoint("host.example.com", endpoint.RecordTypeA, "125.1.1.1"),
}
validateEndpoints(t, actual, expected)
}
@ -391,12 +415,15 @@ func TestInfobloxApplyChanges(t *testing.T) {
endpoint.NewEndpoint("other.com", endpoint.RecordTypeTXT, "tag"),
endpoint.NewEndpoint("new.example.com", endpoint.RecordTypeA, "111.222.111.222"),
endpoint.NewEndpoint("newcname.example.com", endpoint.RecordTypeCNAME, "other.com"),
endpoint.NewEndpoint("multiple.example.com", endpoint.RecordTypeA, "1.2.3.4,3.4.5.6,8.9.10.11"),
endpoint.NewEndpoint("multiple.example.com", endpoint.RecordTypeTXT, "tag-multiple-A-records"),
})
validateEndpoints(t, client.deletedEndpoints, []*endpoint.Endpoint{
endpoint.NewEndpoint("old.example.com", endpoint.RecordTypeA, ""),
endpoint.NewEndpoint("oldcname.example.com", endpoint.RecordTypeCNAME, ""),
endpoint.NewEndpoint("deleted.example.com", endpoint.RecordTypeA, ""),
endpoint.NewEndpoint("deleted.example.com", endpoint.RecordTypeTXT, ""),
endpoint.NewEndpoint("deletedcname.example.com", endpoint.RecordTypeCNAME, ""),
})
@ -424,6 +451,7 @@ func testInfobloxApplyChangesInternal(t *testing.T, dryRun bool, client ibclient
}
client.(*mockIBConnector).mockInfobloxObjects = &[]ibclient.IBObject{
createMockInfobloxObject("deleted.example.com", endpoint.RecordTypeA, "121.212.121.212"),
createMockInfobloxObject("deleted.example.com", endpoint.RecordTypeTXT, "test-deleting-txt"),
createMockInfobloxObject("deletedcname.example.com", endpoint.RecordTypeCNAME, "other.com"),
createMockInfobloxObject("old.example.com", endpoint.RecordTypeA, "121.212.121.212"),
createMockInfobloxObject("oldcname.example.com", endpoint.RecordTypeCNAME, "other.com"),
@ -447,6 +475,8 @@ func testInfobloxApplyChangesInternal(t *testing.T, dryRun bool, client ibclient
endpoint.NewEndpoint("other.com", endpoint.RecordTypeTXT, "tag"),
endpoint.NewEndpoint("nope.com", endpoint.RecordTypeA, "4.4.4.4"),
endpoint.NewEndpoint("nope.com", endpoint.RecordTypeTXT, "tag"),
endpoint.NewEndpoint("multiple.example.com", endpoint.RecordTypeA, "1.2.3.4,3.4.5.6,8.9.10.11"),
endpoint.NewEndpoint("multiple.example.com", endpoint.RecordTypeTXT, "tag-multiple-A-records"),
}
updateOldRecords := []*endpoint.Endpoint{
@ -463,6 +493,7 @@ func testInfobloxApplyChangesInternal(t *testing.T, dryRun bool, client ibclient
deleteRecords := []*endpoint.Endpoint{
endpoint.NewEndpoint("deleted.example.com", endpoint.RecordTypeA, "121.212.121.212"),
endpoint.NewEndpoint("deleted.example.com", endpoint.RecordTypeTXT, "test-deleting-txt"),
endpoint.NewEndpoint("deletedcname.example.com", endpoint.RecordTypeCNAME, "other.com"),
endpoint.NewEndpoint("deleted.nope.com", endpoint.RecordTypeA, "222.111.222.111"),
}

View File

@ -203,7 +203,7 @@ func (p *OCIProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error)
// ApplyChanges applies a given set of changes to a given zone.
func (p *OCIProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
log.Debugf("Processing chages: %+v", changes)
log.Debugf("Processing changes: %+v", changes)
ops := []dns.RecordOperation{}
ops = append(ops, p.newFilteredRecordOperations(changes.Create, dns.RecordOperationOperationAdd)...)

View File

@ -140,8 +140,8 @@ func TestOvhRecords(t *testing.T) {
endpoints, err := provider.Records(context.TODO())
assert.NoError(err)
// Little fix for multi targets endpoint
for _, endoint := range endpoints {
sort.Strings(endoint.Targets)
for _, endpoint := range endpoints {
sort.Strings(endpoint.Targets)
}
assert.ElementsMatch(endpoints, []*endpoint.Endpoint{
{DNSName: "example.org", RecordType: "A", RecordTTL: 10, Labels: endpoint.NewLabels(), Targets: []string{"203.0.113.42"}},

View File

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

View File

@ -125,17 +125,13 @@ var (
endpoint.NewEndpointWithTTL("does.not.exist.com", endpoint.RecordTypeTXT, endpoint.TTL(300), "\"heritage=external-dns,external-dns/owner=tower-pdns\""),
}
endpointsMultipleRecords = []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeA, endpoint.TTL(300), "8.8.8.8"),
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeA, endpoint.TTL(300), "8.8.4.4"),
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeA, endpoint.TTL(300), "4.4.4.4"),
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeA, endpoint.TTL(300), "8.8.8.8", "8.8.4.4", "4.4.4.4"),
}
endpointsMixedRecords = []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("cname.example.com", endpoint.RecordTypeCNAME, endpoint.TTL(300), "example.by.any.other.name.com"),
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeTXT, endpoint.TTL(300), "'would smell as sweet'"),
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeA, endpoint.TTL(300), "8.8.8.8"),
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeA, endpoint.TTL(300), "8.8.4.4"),
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeA, endpoint.TTL(300), "4.4.4.4"),
endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeA, endpoint.TTL(300), "8.8.8.8", "8.8.4.4", "4.4.4.4"),
}
endpointsMultipleZones = []*endpoint.Endpoint{

View File

@ -30,11 +30,16 @@ type Provider interface {
Records(ctx context.Context) ([]*endpoint.Endpoint, error)
ApplyChanges(ctx context.Context, changes *plan.Changes) error
PropertyValuesEqual(name string, previous string, current string) bool
AdjustEndpoints(endpoints []*endpoint.Endpoint) []*endpoint.Endpoint
}
type BaseProvider struct {
}
func (b BaseProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) []*endpoint.Endpoint {
return endpoints
}
func (b BaseProvider) PropertyValuesEqual(name, previous, current string) bool {
return previous == current
}

View File

@ -24,7 +24,11 @@ import (
"strings"
"time"
"github.com/bodgit/tsig"
extendedClient "github.com/bodgit/tsig/client"
"github.com/bodgit/tsig/gss"
"github.com/miekg/dns"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
@ -36,6 +40,9 @@ import (
const (
// maximum size of a UDP transport message in DNS protocol
udpMaxMsgSize = 512
// maximum time DNS client can be off from server for an update to succeed
clockSkew = 300
)
// rfc2136 provider type
@ -50,6 +57,12 @@ type rfc2136Provider struct {
axfr bool
minTTL time.Duration
// options specific to rfc3645 gss-tsig support
gssTsig bool
krb5Username string
krb5Password string
krb5Realm string
// only consider hosted zones managing domains ending in this suffix
domainFilter endpoint.DomainFilter
dryRun bool
@ -72,9 +85,9 @@ type rfc2136Actions interface {
}
// NewRfc2136Provider is a factory function for OpenStack rfc2136 providers
func NewRfc2136Provider(host string, port int, zoneName string, insecure bool, keyName string, secret string, secretAlg string, axfr bool, domainFilter endpoint.DomainFilter, dryRun bool, minTTL time.Duration, actions rfc2136Actions) (provider.Provider, error) {
func NewRfc2136Provider(host string, port int, zoneName string, insecure bool, keyName string, secret string, secretAlg string, axfr bool, domainFilter endpoint.DomainFilter, dryRun bool, minTTL time.Duration, gssTsig bool, krb5Username string, krb5Password string, actions rfc2136Actions) (provider.Provider, error) {
secretAlgChecked, ok := tsigAlgs[secretAlg]
if !ok && !insecure {
if !ok && !insecure && !gssTsig {
return nil, errors.Errorf("%s is not supported TSIG algorithm", secretAlg)
}
@ -82,6 +95,10 @@ func NewRfc2136Provider(host string, port int, zoneName string, insecure bool, k
nameserver: net.JoinHostPort(host, strconv.Itoa(port)),
zoneName: dns.Fqdn(zoneName),
insecure: insecure,
gssTsig: gssTsig,
krb5Username: krb5Username,
krb5Password: krb5Password,
krb5Realm: strings.ToUpper(zoneName),
domainFilter: domainFilter,
dryRun: dryRun,
axfr: axfr,
@ -103,6 +120,22 @@ func NewRfc2136Provider(host string, port int, zoneName string, insecure bool, k
return r, nil
}
// KeyName will return TKEY name and TSIG handle to use for followon actions with a secure connection
func (r rfc2136Provider) KeyData() (keyName *string, handle *gss.GSS, err error) {
handle, err = gss.New()
if err != nil {
return keyName, handle, err
}
rawHost, _, err := net.SplitHostPort(r.nameserver)
if err != nil {
return keyName, handle, err
}
keyName, _, err = handle.NegotiateContextWithCredentials(rawHost, r.krb5Realm, r.krb5Username, r.krb5Password)
return keyName, handle, err
}
// Records returns the list of records.
func (r rfc2136Provider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
rrs, err := r.List()
@ -163,7 +196,7 @@ OuterLoop:
func (r rfc2136Provider) IncomeTransfer(m *dns.Msg, a string) (env chan *dns.Envelope, err error) {
t := new(dns.Transfer)
if !r.insecure {
if !r.insecure && !r.gssTsig {
t.TsigSecret = map[string]string{r.tsigKeyName: r.tsigSecret}
}
@ -180,8 +213,8 @@ func (r rfc2136Provider) List() ([]dns.RR, error) {
m := new(dns.Msg)
m.SetAxfr(r.zoneName)
if !r.insecure {
m.SetTsig(r.tsigKeyName, r.tsigSecretAlg, 300, time.Now().Unix())
if !r.insecure && !r.gssTsig {
m.SetTsig(r.tsigKeyName, r.tsigSecretAlg, clockSkew, time.Now().Unix())
}
env, err := r.actions.IncomeTransfer(m, r.nameserver)
@ -304,12 +337,31 @@ func (r rfc2136Provider) SendMessage(msg *dns.Msg) error {
}
log.Debugf("SendMessage")
c := new(dns.Client)
c := new(extendedClient.Client)
c.SingleInflight = true
if !r.insecure {
c.TsigSecret = map[string]string{r.tsigKeyName: r.tsigSecret}
msg.SetTsig(r.tsigKeyName, r.tsigSecretAlg, 300, time.Now().Unix())
if r.gssTsig {
keyName, handle, err := r.KeyData()
if err != nil {
return err
}
defer handle.Close()
defer handle.DeleteContext(keyName)
c.TsigAlgorithm = map[string]*extendedClient.TsigAlgorithm{
tsig.GSS: {
Generate: handle.GenerateGSS,
Verify: handle.VerifyGSS,
},
}
c.TsigSecret = map[string]string{*keyName: ""}
msg.SetTsig(*keyName, tsig.GSS, clockSkew, time.Now().Unix())
} else {
c.TsigSecret = map[string]string{r.tsigKeyName: r.tsigSecret}
msg.SetTsig(r.tsigKeyName, r.tsigSecretAlg, clockSkew, time.Now().Unix())
}
}
if msg.Len() > udpMaxMsgSize {
@ -318,8 +370,11 @@ func (r rfc2136Provider) SendMessage(msg *dns.Msg) error {
resp, _, err := c.Exchange(msg, r.nameserver)
if err != nil {
log.Infof("error in dns.Client.Exchange: %s", err)
return err
if resp != nil && resp.Rcode != dns.RcodeSuccess {
log.Infof("error in dns.Client.Exchange: %s", err)
return err
}
log.Warnf("warn in dns.Client.Exchange: %s", err)
}
if resp != nil && resp.Rcode != dns.RcodeSuccess {
log.Infof("Bad dns.Client.Exchange response: %s", resp)

View File

@ -95,7 +95,7 @@ func (r *rfc2136Stub) IncomeTransfer(m *dns.Msg, a string) (env chan *dns.Envelo
}
func createRfc2136StubProvider(stub *rfc2136Stub) (provider.Provider, error) {
return NewRfc2136Provider("", 0, "", false, "key", "secret", "hmac-sha512", true, endpoint.DomainFilter{}, false, 300*time.Second, stub)
return NewRfc2136Provider("", 0, "", false, "key", "secret", "hmac-sha512", true, endpoint.DomainFilter{}, false, 300*time.Second, false, "", "", stub)
}
func extractAuthoritySectionFromMessage(msg fmt.Stringer) []string {

View File

@ -17,7 +17,7 @@ limitations under the License.
package scaleway
import (
domain "github.com/scaleway/scaleway-sdk-go/api/domain/v2alpha2"
domain "github.com/scaleway/scaleway-sdk-go/api/domain/v2beta1"
"github.com/scaleway/scaleway-sdk-go/scw"
)

View File

@ -22,7 +22,7 @@ import (
"strconv"
"strings"
domain "github.com/scaleway/scaleway-sdk-go/api/domain/v2alpha2"
domain "github.com/scaleway/scaleway-sdk-go/api/domain/v2beta1"
"github.com/scaleway/scaleway-sdk-go/scw"
log "github.com/sirupsen/logrus"
@ -84,6 +84,21 @@ func NewScalewayProvider(ctx context.Context, domainFilter endpoint.DomainFilter
}, nil
}
// AdjustEndpoints is used to normalize the endoints
func (p *ScalewayProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) []*endpoint.Endpoint {
eps := make([]*endpoint.Endpoint, len(endpoints))
for i := range endpoints {
eps[i] = endpoints[i]
if !eps[i].RecordTTL.IsConfigured() {
eps[i].RecordTTL = endpoint.TTL(scalewyRecordTTL)
}
if _, ok := eps[i].GetProviderSpecificProperty(scalewayPriorityKey); !ok {
eps[i] = eps[i].WithProviderSpecific(scalewayPriorityKey, fmt.Sprintf("%d", scalewayDefaultPriority))
}
}
return eps
}
// Zones returns the list of hosted zones.
func (p *ScalewayProvider) Zones(ctx context.Context) ([]*domain.DNSZone, error) {
res := []*domain.DNSZone{}
@ -136,7 +151,7 @@ func (p *ScalewayProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, e
// In this case, we juste take the first one.
if existingEndpoint, ok := endpoints[record.Type.String()+"/"+fullRecordName]; ok {
existingEndpoint.Targets = append(existingEndpoint.Targets, record.Data)
log.Infof("Appending target %s to record %s, using TTL and priotiry of target %s", record.Data, fullRecordName, existingEndpoint.Targets[0])
log.Infof("Appending target %s to record %s, using TTL and priority of target %s", record.Data, fullRecordName, existingEndpoint.Targets[0])
} else {
ep := endpoint.NewEndpointWithTTL(fullRecordName, record.Type.String(), endpoint.TTL(record.TTL), record.Data)
ep = ep.WithProviderSpecific(scalewayPriorityKey, fmt.Sprintf("%d", record.Priority))
@ -192,6 +207,7 @@ func (p *ScalewayProvider) generateApplyRequests(ctx context.Context, changes *p
recordsToDelete[zoneName] = []*domain.RecordChange{}
}
log.Debugf("Following records present in updateOld")
for _, c := range changes.UpdateOld {
zone, _ := zoneNameMapper.FindZone(c.DNSName)
if zone == "" {
@ -199,8 +215,10 @@ func (p *ScalewayProvider) generateApplyRequests(ctx context.Context, changes *p
continue
}
recordsToDelete[zone] = append(recordsToDelete[zone], endpointToScalewayRecordsChangeDelete(zone, c)...)
log.Debugf("%s", c.String())
}
log.Debugf("Following records present in delete")
for _, c := range changes.Delete {
zone, _ := zoneNameMapper.FindZone(c.DNSName)
if zone == "" {
@ -208,8 +226,10 @@ func (p *ScalewayProvider) generateApplyRequests(ctx context.Context, changes *p
continue
}
recordsToDelete[zone] = append(recordsToDelete[zone], endpointToScalewayRecordsChangeDelete(zone, c)...)
log.Debugf("%s", c.String())
}
log.Debugf("Following records present in create")
for _, c := range changes.Create {
zone, _ := zoneNameMapper.FindZone(c.DNSName)
if zone == "" {
@ -217,7 +237,10 @@ func (p *ScalewayProvider) generateApplyRequests(ctx context.Context, changes *p
continue
}
recordsToAdd[zone].Records = append(recordsToAdd[zone].Records, endpointToScalewayRecords(zone, c)...)
log.Debugf("%s", c.String())
}
log.Debugf("Following records present in updateNew")
for _, c := range changes.UpdateNew {
zone, _ := zoneNameMapper.FindZone(c.DNSName)
if zone == "" {
@ -225,6 +248,7 @@ func (p *ScalewayProvider) generateApplyRequests(ctx context.Context, changes *p
continue
}
recordsToAdd[zone].Records = append(recordsToAdd[zone].Records, endpointToScalewayRecords(zone, c)...)
log.Debugf("%s", c.String())
}
for _, zone := range dnsZones {
@ -269,8 +293,13 @@ func endpointToScalewayRecords(zoneName string, ep *endpoint.Endpoint) []*domain
records := []*domain.Record{}
for _, target := range ep.Targets {
finalTargetName := target
if domain.RecordType(ep.RecordType) == domain.RecordTypeCNAME {
finalTargetName = provider.EnsureTrailingDot(target)
}
records = append(records, &domain.Record{
Data: target,
Data: finalTargetName,
Name: strings.Trim(strings.TrimSuffix(ep.DNSName, zoneName), ". "),
Priority: priority,
TTL: ttl,
@ -285,11 +314,18 @@ func endpointToScalewayRecordsChangeDelete(zoneName string, ep *endpoint.Endpoin
records := []*domain.RecordChange{}
for _, target := range ep.Targets {
finalTargetName := target
if domain.RecordType(ep.RecordType) == domain.RecordTypeCNAME {
finalTargetName = provider.EnsureTrailingDot(target)
}
records = append(records, &domain.RecordChange{
Delete: &domain.RecordChangeDelete{
Data: target,
Name: strings.Trim(strings.TrimSuffix(ep.DNSName, zoneName), ". "),
Type: domain.RecordType(ep.RecordType),
IDFields: &domain.RecordIdentifier{
Data: &finalTargetName,
Name: strings.Trim(strings.TrimSuffix(ep.DNSName, zoneName), ". "),
Type: domain.RecordType(ep.RecordType),
},
},
})
}
@ -298,10 +334,10 @@ func endpointToScalewayRecordsChangeDelete(zoneName string, ep *endpoint.Endpoin
}
func logChanges(req *domain.UpdateDNSZoneRecordsRequest) {
log.Infof("Updating zone %s", req.DNSZone)
if !log.IsLevelEnabled(log.InfoLevel) {
return
}
log.Infof("Updating zone %s", req.DNSZone)
for _, change := range req.Changes {
if change.Add != nil {
for _, add := range change.Add.Records {
@ -320,15 +356,15 @@ func logChanges(req *domain.UpdateDNSZoneRecordsRequest) {
log.WithFields(logFields).Info("Adding record")
}
} else if change.Delete != nil {
name := change.Delete.Name + "."
if change.Delete.Name == "" {
name := change.Delete.IDFields.Name + "."
if change.Delete.IDFields.Name == "" {
name = ""
}
logFields := log.Fields{
"record": name + req.DNSZone,
"type": change.Delete.Type.String(),
"data": change.Delete.Data,
"type": change.Delete.IDFields.Type.String(),
"data": *change.Delete.IDFields.Data,
}
log.WithFields(logFields).Info("Deleting record")

View File

@ -22,7 +22,7 @@ import (
"reflect"
"testing"
domain "github.com/scaleway/scaleway-sdk-go/api/domain/v2alpha2"
domain "github.com/scaleway/scaleway-sdk-go/api/domain/v2beta1"
"github.com/scaleway/scaleway-sdk-go/scw"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -93,7 +93,7 @@ func (m *mockScalewayDomain) ListDNSZoneRecords(req *domain.ListDNSZoneRecordsRe
Type: domain.RecordTypeA,
},
{
Data: "test.example.com",
Data: "test.example.com.",
Name: "two",
TTL: 600,
Priority: 30,
@ -158,6 +158,90 @@ func TestScalewayProvider_NewScalewayProvider(t *testing.T) {
}
}
func TestScalewayProvider_AdjustEndpoints(t *testing.T) {
provider := &ScalewayProvider{}
before := []*endpoint.Endpoint{
{
DNSName: "one.example.com",
RecordTTL: 300,
RecordType: "A",
Targets: []string{"1.1.1.1"},
ProviderSpecific: endpoint.ProviderSpecific{
{
Name: scalewayPriorityKey,
Value: "0",
},
},
},
{
DNSName: "two.example.com",
RecordTTL: 0,
RecordType: "A",
Targets: []string{"1.1.1.1"},
ProviderSpecific: endpoint.ProviderSpecific{
{
Name: scalewayPriorityKey,
Value: "10",
},
},
},
{
DNSName: "three.example.com",
RecordTTL: 600,
RecordType: "A",
Targets: []string{"1.1.1.1"},
ProviderSpecific: endpoint.ProviderSpecific{},
},
}
expected := []*endpoint.Endpoint{
{
DNSName: "one.example.com",
RecordTTL: 300,
RecordType: "A",
Targets: []string{"1.1.1.1"},
ProviderSpecific: endpoint.ProviderSpecific{
{
Name: scalewayPriorityKey,
Value: "0",
},
},
},
{
DNSName: "two.example.com",
RecordTTL: 300,
RecordType: "A",
Targets: []string{"1.1.1.1"},
ProviderSpecific: endpoint.ProviderSpecific{
{
Name: scalewayPriorityKey,
Value: "10",
},
},
},
{
DNSName: "three.example.com",
RecordTTL: 600,
RecordType: "A",
Targets: []string{"1.1.1.1"},
ProviderSpecific: endpoint.ProviderSpecific{
{
Name: scalewayPriorityKey,
Value: "0",
},
},
},
}
after := provider.AdjustEndpoints(before)
for i := range after {
if !checkRecordEquality(after[i], expected[i]) {
t.Errorf("got record %s instead of %s", after[i], expected[i])
}
}
}
func TestScalewayProvider_Zones(t *testing.T) {
mocked := mockScalewayDomain{nil}
provider := &ScalewayProvider{
@ -302,23 +386,29 @@ func TestScalewayProvider_generateApplyRequests(t *testing.T) {
},
{
Delete: &domain.RecordChangeDelete{
Data: "3.3.3.3",
Name: "me",
Type: domain.RecordTypeA,
IDFields: &domain.RecordIdentifier{
Data: scw.StringPtr("3.3.3.3"),
Name: "me",
Type: domain.RecordTypeA,
},
},
},
{
Delete: &domain.RecordChangeDelete{
Data: "1.1.1.1",
Name: "here",
Type: domain.RecordTypeA,
IDFields: &domain.RecordIdentifier{
Data: scw.StringPtr("1.1.1.1"),
Name: "here",
Type: domain.RecordTypeA,
},
},
},
{
Delete: &domain.RecordChangeDelete{
Data: "1.1.1.2",
Name: "here",
Type: domain.RecordTypeA,
IDFields: &domain.RecordIdentifier{
Data: scw.StringPtr("1.1.1.2"),
Name: "here",
Type: domain.RecordTypeA,
},
},
},
},
@ -330,7 +420,7 @@ func TestScalewayProvider_generateApplyRequests(t *testing.T) {
Add: &domain.RecordChangeAdd{
Records: []*domain.Record{
{
Data: "example.com",
Data: "example.com.",
Name: "",
TTL: 600,
Type: domain.RecordTypeCNAME,
@ -355,23 +445,29 @@ func TestScalewayProvider_generateApplyRequests(t *testing.T) {
},
{
Delete: &domain.RecordChangeDelete{
Data: "1.1.1.1",
Name: "here.is.my",
Type: domain.RecordTypeA,
IDFields: &domain.RecordIdentifier{
Data: scw.StringPtr("1.1.1.1"),
Name: "here.is.my",
Type: domain.RecordTypeA,
},
},
},
{
Delete: &domain.RecordChangeDelete{
Data: "4.4.4.4",
Name: "my",
Type: domain.RecordTypeA,
IDFields: &domain.RecordIdentifier{
Data: scw.StringPtr("4.4.4.4"),
Name: "my",
Type: domain.RecordTypeA,
},
},
},
{
Delete: &domain.RecordChangeDelete{
Data: "5.5.5.5",
Name: "my",
Type: domain.RecordTypeA,
IDFields: &domain.RecordIdentifier{
Data: scw.StringPtr("5.5.5.5"),
Name: "my",
Type: domain.RecordTypeA,
},
},
},
},
@ -488,7 +584,7 @@ func checkScalewayReqChanges(r1, r2 *domain.UpdateDNSZoneRecordsRequest) bool {
if c1.Add != nil && c2.Add != nil && checkScalewayRecords(c1.Add.Records, c2.Add.Records) {
total--
} else if c1.Delete != nil && c2.Delete != nil {
if c1.Delete.Data == c2.Delete.Data && c1.Delete.Name == c2.Delete.Name && c1.Delete.Type == c2.Delete.Type {
if *c1.Delete.IDFields.Data == *c2.Delete.IDFields.Data && c1.Delete.IDFields.Name == c2.Delete.IDFields.Name && c1.Delete.IDFields.Type == c2.Delete.IDFields.Type {
total--
}
}

View File

@ -263,7 +263,7 @@ func (p *TransIPProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, er
}
// endpointNameForRecord returns "www.example.org" for DNSEntry with Name "www" and
// Doman with Name "example.org"
// Domain with Name "example.org"
func (p *TransIPProvider) endpointNameForRecord(r transip.DNSEntry, d transip.Domain) string {
// root name is identified by "@" and should be translated to domain name for
// the endpoint entry.

View File

@ -231,7 +231,7 @@ func TestUltraDNSProvider_ApplyChangesCNAME(t *testing.T) {
assert.NotNil(t, err)
}
// This will work if you would set the environment variables such as "ULTRADNS_INTEGRATION" and zone should be avaialble "kubernetes-ultradns-provider-test.com"
// This will work if you would set the environment variables such as "ULTRADNS_INTEGRATION" and zone should be available "kubernetes-ultradns-provider-test.com"
func TestUltraDNSProvider_ApplyChanges_Integration(t *testing.T) {
_, ok := os.LookupEnv("ULTRADNS_INTEGRATION")
@ -311,7 +311,7 @@ func TestUltraDNSProvider_ApplyChanges_Integration(t *testing.T) {
}
// This will work if you would set the environment variables such as "ULTRADNS_INTEGRATION" and zone should be avaialble "kubernetes-ultradns-provider-test.com" for multiple target
// This will work if you would set the environment variables such as "ULTRADNS_INTEGRATION" and zone should be available "kubernetes-ultradns-provider-test.com" for multiple target
func TestUltraDNSProvider_ApplyChanges_MultipleTarget_integeration(t *testing.T) {
_, ok := os.LookupEnv("ULTRADNS_INTEGRATION")
if !ok {
@ -652,7 +652,7 @@ func TestUltraDNSProvider_PoolConversionCase(t *testing.T) {
resp, _ := provider.client.Do("GET", "zones/kubernetes-ultradns-provider-test.com./rrsets/A/ttl.kubernetes-ultradns-provider-test.com.", nil, udnssdk.RRSetListDTO{})
assert.Equal(t, resp.Status, "200 OK")
//Coverting to RD Pool
//Converting to RD Pool
_ = os.Setenv("ULTRADNS_POOL_TYPE", "rdpool")
provider, _ = NewUltraDNSProvider(endpoint.NewDomainFilter([]string{"kubernetes-ultradns-provider-test.com"}), false)
changes = &plan.Changes{}
@ -662,7 +662,7 @@ func TestUltraDNSProvider_PoolConversionCase(t *testing.T) {
resp, _ = provider.client.Do("GET", "zones/kubernetes-ultradns-provider-test.com./rrsets/A/ttl.kubernetes-ultradns-provider-test.com.", nil, udnssdk.RRSetListDTO{})
assert.Equal(t, resp.Status, "200 OK")
//Coverting back to SB Pool
//Converting back to SB Pool
_ = os.Setenv("ULTRADNS_POOL_TYPE", "sbpool")
provider, _ = NewUltraDNSProvider(endpoint.NewDomainFilter([]string{"kubernetes-ultradns-provider-test.com"}), false)
changes = &plan.Changes{}

View File

@ -91,3 +91,8 @@ func (sdr *AWSSDRegistry) updateLabels(endpoints []*endpoint.Endpoint) {
func (sdr *AWSSDRegistry) PropertyValuesEqual(name string, previous string, current string) bool {
return sdr.provider.PropertyValuesEqual(name, previous, current)
}
// AdjustEndpoints modifies the endpoints as needed by the specific provider
func (sdr *AWSSDRegistry) AdjustEndpoints(endpoints []*endpoint.Endpoint) []*endpoint.Endpoint {
return sdr.provider.AdjustEndpoints(endpoints)
}

View File

@ -50,3 +50,8 @@ func (im *NoopRegistry) ApplyChanges(ctx context.Context, changes *plan.Changes)
func (im *NoopRegistry) PropertyValuesEqual(attribute string, previous string, current string) bool {
return im.provider.PropertyValuesEqual(attribute, previous, current)
}
// AdjustEndpoints modifies the endpoints as needed by the specific provider
func (im *NoopRegistry) AdjustEndpoints(endpoints []*endpoint.Endpoint) []*endpoint.Endpoint {
return im.provider.AdjustEndpoints(endpoints)
}

View File

@ -33,6 +33,7 @@ type Registry interface {
Records(ctx context.Context) ([]*endpoint.Endpoint, error)
ApplyChanges(ctx context.Context, changes *plan.Changes) error
PropertyValuesEqual(attribute string, previous string, current string) bool
AdjustEndpoints(endpoints []*endpoint.Endpoint) []*endpoint.Endpoint
}
//TODO(ideahitme): consider moving this to Plan

View File

@ -40,10 +40,15 @@ type TXTRegistry struct {
recordsCache []*endpoint.Endpoint
recordsCacheRefreshTime time.Time
cacheInterval time.Duration
// optional string to use to replace the asterisk in wildcard entries - without using this,
// registry TXT records corresponding to wildcard records will be invalid (and rejected by most providers), due to
// having a '*' appear (not as the first character) - see https://tools.ietf.org/html/rfc1034#section-4.3.3
wildcardReplacement string
}
// NewTXTRegistry returns new TXTRegistry object
func NewTXTRegistry(provider provider.Provider, txtPrefix, txtSuffix, ownerID string, cacheInterval time.Duration) (*TXTRegistry, error) {
func NewTXTRegistry(provider provider.Provider, txtPrefix, txtSuffix, ownerID string, cacheInterval time.Duration, txtWildcardReplacement string) (*TXTRegistry, error) {
if ownerID == "" {
return nil, errors.New("owner id cannot be empty")
}
@ -52,13 +57,14 @@ func NewTXTRegistry(provider provider.Provider, txtPrefix, txtSuffix, ownerID st
return nil, errors.New("txt-prefix and txt-suffix are mutual exclusive")
}
mapper := newaffixNameMapper(txtPrefix, txtSuffix)
mapper := newaffixNameMapper(txtPrefix, txtSuffix, txtWildcardReplacement)
return &TXTRegistry{
provider: provider,
ownerID: ownerID,
mapper: mapper,
cacheInterval: cacheInterval,
provider: provider,
ownerID: ownerID,
mapper: mapper,
cacheInterval: cacheInterval,
wildcardReplacement: txtWildcardReplacement,
}, nil
}
@ -107,7 +113,13 @@ func (im *TXTRegistry) Records(ctx context.Context) ([]*endpoint.Endpoint, error
if ep.Labels == nil {
ep.Labels = endpoint.NewLabels()
}
key := fmt.Sprintf("%s::%s", ep.DNSName, ep.SetIdentifier)
dnsNameSplit := strings.Split(ep.DNSName, ".")
// If specified, replace a leading asterisk in the generated txt record name with some other string
if im.wildcardReplacement != "" && dnsNameSplit[0] == "*" {
dnsNameSplit[0] = im.wildcardReplacement
}
dnsName := strings.Join(dnsNameSplit, ".")
key := fmt.Sprintf("%s::%s", dnsName, ep.SetIdentifier)
if labels, ok := labelMap[key]; ok {
for k, v := range labels {
ep.Labels[k] = v
@ -196,6 +208,11 @@ func (im *TXTRegistry) PropertyValuesEqual(name string, previous string, current
return im.provider.PropertyValuesEqual(name, previous, current)
}
// AdjustEndpoints modifies the endpoints as needed by the specific provider
func (im *TXTRegistry) AdjustEndpoints(endpoints []*endpoint.Endpoint) []*endpoint.Endpoint {
return im.provider.AdjustEndpoints(endpoints)
}
/**
TXT registry specific private methods
*/
@ -211,14 +228,15 @@ type nameMapper interface {
}
type affixNameMapper struct {
prefix string
suffix string
prefix string
suffix string
wildcardReplacement string
}
var _ nameMapper = affixNameMapper{}
func newaffixNameMapper(prefix string, suffix string) affixNameMapper {
return affixNameMapper{prefix: strings.ToLower(prefix), suffix: strings.ToLower(suffix)}
func newaffixNameMapper(prefix string, suffix string, wildcardReplacement string) affixNameMapper {
return affixNameMapper{prefix: strings.ToLower(prefix), suffix: strings.ToLower(suffix), wildcardReplacement: strings.ToLower(wildcardReplacement)}
}
func (pr affixNameMapper) toEndpointName(txtDNSName string) string {
@ -238,6 +256,12 @@ func (pr affixNameMapper) toEndpointName(txtDNSName string) string {
func (pr affixNameMapper) toTXTName(endpointDNSName string) string {
DNSName := strings.SplitN(endpointDNSName, ".", 2)
// If specified, replace a leading asterisk in the generated txt record name with some other string
if pr.wildcardReplacement != "" && DNSName[0] == "*" {
DNSName[0] = pr.wildcardReplacement
}
if len(DNSName) < 2 {
return pr.prefix + DNSName[0] + pr.suffix
}

View File

@ -44,20 +44,20 @@ func TestTXTRegistry(t *testing.T) {
func testTXTRegistryNew(t *testing.T) {
p := inmemory.NewInMemoryProvider()
_, err := NewTXTRegistry(p, "txt", "", "", time.Hour)
_, err := NewTXTRegistry(p, "txt", "", "", time.Hour, "")
require.Error(t, err)
_, err = NewTXTRegistry(p, "", "txt", "", time.Hour)
_, err = NewTXTRegistry(p, "", "txt", "", time.Hour, "")
require.Error(t, err)
r, err := NewTXTRegistry(p, "txt", "", "owner", time.Hour)
r, err := NewTXTRegistry(p, "txt", "", "owner", time.Hour, "")
require.NoError(t, err)
assert.Equal(t, p, r.provider)
r, err = NewTXTRegistry(p, "", "txt", "owner", time.Hour)
r, err = NewTXTRegistry(p, "", "txt", "owner", time.Hour, "")
require.NoError(t, err)
_, err = NewTXTRegistry(p, "txt", "txt", "owner", time.Hour)
_, err = NewTXTRegistry(p, "txt", "txt", "owner", time.Hour, "")
require.Error(t, err)
_, ok := r.mapper.(affixNameMapper)
@ -65,7 +65,7 @@ func testTXTRegistryNew(t *testing.T) {
assert.Equal(t, "owner", r.ownerID)
assert.Equal(t, p, r.provider)
r, err = NewTXTRegistry(p, "", "", "owner", time.Hour)
r, err = NewTXTRegistry(p, "", "", "owner", time.Hour, "")
require.NoError(t, err)
_, ok = r.mapper.(affixNameMapper)
@ -97,6 +97,8 @@ func testTXTRegistryRecordsPrefixed(t *testing.T) {
newEndpointWithOwner("multiple.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-1"),
newEndpointWithOwner("multiple.test-zone.example.org", "lb2.loadbalancer.com", endpoint.RecordTypeCNAME, "").WithSetIdentifier("test-set-2"),
newEndpointWithOwner("multiple.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-2"),
newEndpointWithOwner("*.wildcard.test-zone.example.org", "foo.loadbalancer.com", endpoint.RecordTypeCNAME, ""),
newEndpointWithOwner("txt.wc.wildcard.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""),
},
})
expectedRecords := []*endpoint.Endpoint{
@ -169,15 +171,23 @@ func testTXTRegistryRecordsPrefixed(t *testing.T) {
endpoint.OwnerLabelKey: "",
},
},
{
DNSName: "*.wildcard.test-zone.example.org",
Targets: endpoint.Targets{"foo.loadbalancer.com"},
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "owner",
},
},
}
r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour)
r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "wc")
records, _ := r.Records(ctx)
assert.True(t, testutils.SameEndpoints(records, expectedRecords))
// Ensure prefix is case-insensitive
r, _ = NewTXTRegistry(p, "TxT.", "", "owner", time.Hour)
r, _ = NewTXTRegistry(p, "TxT.", "", "owner", time.Hour, "")
records, _ = r.Records(ctx)
assert.True(t, testutils.SameEndpointLabels(records, expectedRecords))
@ -276,13 +286,13 @@ func testTXTRegistryRecordsSuffixed(t *testing.T) {
},
}
r, _ := NewTXTRegistry(p, "", "-txt", "owner", time.Hour)
r, _ := NewTXTRegistry(p, "", "-txt", "owner", time.Hour, "")
records, _ := r.Records(ctx)
assert.True(t, testutils.SameEndpoints(records, expectedRecords))
// Ensure prefix is case-insensitive
r, _ = NewTXTRegistry(p, "", "-TxT", "owner", time.Hour)
r, _ = NewTXTRegistry(p, "", "-TxT", "owner", time.Hour, "")
records, _ = r.Records(ctx)
assert.True(t, testutils.SameEndpointLabels(records, expectedRecords))
@ -357,7 +367,7 @@ func testTXTRegistryRecordsNoPrefix(t *testing.T) {
},
}
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour)
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "")
records, _ := r.Records(ctx)
assert.True(t, testutils.SameEndpoints(records, expectedRecords))
@ -394,7 +404,7 @@ func testTXTRegistryApplyChangesWithPrefix(t *testing.T) {
newEndpointWithOwner("txt.multiple.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-2"),
},
})
r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour)
r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "")
changes := &plan.Changes{
Create: []*endpoint.Endpoint{
@ -488,13 +498,14 @@ func testTXTRegistryApplyChangesWithSuffix(t *testing.T) {
newEndpointWithOwner("multiple-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-2"),
},
})
r, _ := NewTXTRegistry(p, "", "-txt", "owner", time.Hour)
r, _ := NewTXTRegistry(p, "", "-txt", "owner", time.Hour, "wildcard")
changes := &plan.Changes{
Create: []*endpoint.Endpoint{
newEndpointWithOwnerResource("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", "", "", "ingress/default/my-ingress"),
newEndpointWithOwnerResource("multiple.test-zone.example.org", "lb3.loadbalancer.com", "", "", "ingress/default/my-ingress").WithSetIdentifier("test-set-3"),
newEndpointWithOwnerResource("example", "new-loadbalancer-1.lb.com", "", "", "ingress/default/my-ingress"),
newEndpointWithOwnerResource("*.wildcard.test-zone.example.org", "new-loadbalancer-1.lb.com", "", "", "ingress/default/my-ingress"),
},
Delete: []*endpoint.Endpoint{
newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"),
@ -517,6 +528,8 @@ func testTXTRegistryApplyChangesWithSuffix(t *testing.T) {
newEndpointWithOwner("multiple-txt.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, "").WithSetIdentifier("test-set-3"),
newEndpointWithOwnerResource("example", "new-loadbalancer-1.lb.com", "", "owner", "ingress/default/my-ingress"),
newEndpointWithOwner("example-txt", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, ""),
newEndpointWithOwnerResource("*.wildcard.test-zone.example.org", "new-loadbalancer-1.lb.com", "", "owner", "ingress/default/my-ingress"),
newEndpointWithOwner("wildcard-txt.wildcard.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, ""),
},
Delete: []*endpoint.Endpoint{
newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"),
@ -578,7 +591,7 @@ func testTXTRegistryApplyChangesNoPrefix(t *testing.T) {
newEndpointWithOwner("foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""),
},
})
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour)
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "")
changes := &plan.Changes{
Create: []*endpoint.Endpoint{

View File

@ -20,6 +20,14 @@ function generate_changelog {
pr_author="$(gh pr view "$pr_num" | grep author | awk '{ print $2 }' | tr $'\n' ' ')"
printf "* %s (%s) @%s\n\n" "$pr_desc" "$pr_num" "$pr_author"
done
git log "$previous_tag".. --reverse --oneline --grep='(#' | \
while read -r sha title; do
pr_num="$(grep -o '#[[:digit:]]\+' <<<"$title")"
pr_desc="$(git show -s --format=%s "$sha")"
pr_author="$(gh pr view "$pr_num" | grep author | awk '{ print $2 }' | tr $'\n' ' ')"
printf "* %s (%s) @%s\n\n" "$pr_desc" "$pr_num" "$pr_author"
done
}
function create_release {

283
source/ambassador_host.go Normal file
View File

@ -0,0 +1,283 @@
/*
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 source
import (
"context"
"fmt"
"sort"
"strings"
"time"
ambassador "github.com/datawire/ambassador/pkg/api/getambassador.io/v2"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/dynamic/dynamicinformer"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/tools/cache"
api "k8s.io/kubernetes/pkg/apis/core"
"sigs.k8s.io/external-dns/endpoint"
)
// ambHostAnnotation is the annotation in the Host that maps to a Service
const ambHostAnnotation = "external-dns.ambassador-service"
// groupName is the group name for the Ambassador API
const groupName = "getambassador.io"
var schemeGroupVersion = schema.GroupVersion{Group: groupName, Version: "v2"}
var ambHostGVR = schemeGroupVersion.WithResource("hosts")
// ambassadorHostSource is an implementation of Source for Ambassador Host objects.
// The IngressRoute implementation uses the spec.virtualHost.fqdn value for the hostname.
// Use targetAnnotationKey to explicitly set Endpoint.
type ambassadorHostSource struct {
dynamicKubeClient dynamic.Interface
kubeClient kubernetes.Interface
namespace string
ambassadorHostInformer informers.GenericInformer
unstructuredConverter *unstructuredConverter
}
// NewAmbassadorHostSource creates a new ambassadorHostSource with the given config.
func NewAmbassadorHostSource(
dynamicKubeClient dynamic.Interface,
kubeClient kubernetes.Interface,
namespace string) (Source, error) {
var err error
// Use shared informer to listen for add/update/delete of Host in the specified namespace.
// Set resync period to 0, to prevent processing when nothing has changed.
informerFactory := dynamicinformer.NewFilteredDynamicSharedInformerFactory(dynamicKubeClient, 0, namespace, nil)
ambassadorHostInformer := informerFactory.ForResource(ambHostGVR)
// Add default resource event handlers to properly initialize informer.
ambassadorHostInformer.Informer().AddEventHandler(
cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
},
},
)
// TODO informer is not explicitly stopped since controller is not passing in its channel.
informerFactory.Start(wait.NeverStop)
// wait for the local cache to be populated.
err = poll(time.Second, 60*time.Second, func() (bool, error) {
return ambassadorHostInformer.Informer().HasSynced(), nil
})
if err != nil {
return nil, errors.Wrapf(err, "failed to sync cache")
}
uc, err := newUnstructuredConverter()
if err != nil {
return nil, errors.Wrapf(err, "failed to setup Unstructured Converter")
}
return &ambassadorHostSource{
dynamicKubeClient: dynamicKubeClient,
kubeClient: kubeClient,
namespace: namespace,
ambassadorHostInformer: ambassadorHostInformer,
unstructuredConverter: uc,
}, nil
}
// Endpoints returns endpoint objects for each host-target combination that should be processed.
// Retrieves all Hosts in the source's namespace(s).
func (sc *ambassadorHostSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) {
hosts, err := sc.ambassadorHostInformer.Lister().ByNamespace(sc.namespace).List(labels.Everything())
if err != nil {
return nil, err
}
endpoints := []*endpoint.Endpoint{}
for _, hostObj := range hosts {
unstructuredHost, ok := hostObj.(*unstructured.Unstructured)
if !ok {
return nil, errors.New("could not convert")
}
host := &ambassador.Host{}
err := sc.unstructuredConverter.scheme.Convert(unstructuredHost, host, nil)
if err != nil {
return nil, err
}
fullname := fmt.Sprintf("%s/%s", host.Namespace, host.Name)
// look for the "exernal-dns.ambassador-service" annotation. If it is not there then just ignore this `Host`
service, found := host.Annotations[ambHostAnnotation]
if !found {
log.Debugf("Host %s ignored: no annotation %q found", fullname, ambHostAnnotation)
continue
}
targets, err := sc.targetsFromAmbassadorLoadBalancer(ctx, service)
if err != nil {
return nil, err
}
hostEndpoints, err := sc.endpointsFromHost(ctx, host, targets)
if err != nil {
return nil, err
}
if len(hostEndpoints) == 0 {
log.Debugf("No endpoints could be generated from Host %s", fullname)
continue
}
log.Debugf("Endpoints generated from Host: %s: %v", fullname, hostEndpoints)
endpoints = append(endpoints, hostEndpoints...)
}
for _, ep := range endpoints {
sort.Sort(ep.Targets)
}
return endpoints, nil
}
// endpointsFromHost extracts the endpoints from a Host object
func (sc *ambassadorHostSource) endpointsFromHost(ctx context.Context, host *ambassador.Host, targets endpoint.Targets) ([]*endpoint.Endpoint, error) {
var endpoints []*endpoint.Endpoint
providerSpecific := endpoint.ProviderSpecific{}
setIdentifier := ""
annotations := host.Annotations
ttl, err := getTTLFromAnnotations(annotations)
if err != nil {
return nil, err
}
if host.Spec != nil {
hostname := host.Spec.Hostname
if hostname != "" {
endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier)...)
}
}
return endpoints, nil
}
func (sc *ambassadorHostSource) targetsFromAmbassadorLoadBalancer(ctx context.Context, service string) (targets endpoint.Targets, err error) {
lbNamespace, lbName, err := parseAmbLoadBalancerService(service)
if err != nil {
return nil, err
}
svc, err := sc.kubeClient.CoreV1().Services(lbNamespace).Get(ctx, lbName, metav1.GetOptions{})
if err != nil {
return nil, err
}
for _, lb := range svc.Status.LoadBalancer.Ingress {
if lb.IP != "" {
targets = append(targets, lb.IP)
}
if lb.Hostname != "" {
targets = append(targets, lb.Hostname)
}
}
return
}
// parseAmbLoadBalancerService returns a name/namespace tuple from the annotation in
// an Ambassador Host CRD
//
// This is a thing because Ambassador has historically supported cross-namespace
// references using a name.namespace syntax, but here we want to also support
// namespace/name.
//
// Returns namespace, name, error.
func parseAmbLoadBalancerService(service string) (namespace, name string, err error) {
// Start by assuming that we have namespace/name.
parts := strings.Split(service, "/")
if len(parts) == 1 {
// No "/" at all, so let's try for name.namespace. To be consistent with the
// rest of Ambassador, use SplitN to limit this to one split, so that e.g.
// svc.foo.bar uses service "svc" in namespace "foo.bar".
parts = strings.SplitN(service, ".", 2)
if len(parts) == 2 {
// We got a namespace, great.
name := parts[0]
namespace := parts[1]
return namespace, name, nil
}
// If here, we have no separator, so the whole string is the service, and
// we can assume the default namespace.
name := service
namespace := api.NamespaceDefault
return namespace, name, nil
} else if len(parts) == 2 {
// This is "namespace/name". Note that the name could be qualified,
// which is fine.
namespace := parts[0]
name := parts[1]
return namespace, name, nil
}
// If we got here, this string is simply ill-formatted. Return an error.
return "", "", errors.New(fmt.Sprintf("invalid external-dns service: %s", service))
}
func (sc *ambassadorHostSource) AddEventHandler(ctx context.Context, handler func()) {
}
// unstructuredConverter handles conversions between unstructured.Unstructured and Ambassador types
type unstructuredConverter struct {
// scheme holds an initializer for converting Unstructured to a type
scheme *runtime.Scheme
}
// newUnstructuredConverter returns a new unstructuredConverter initialized
func newUnstructuredConverter() (*unstructuredConverter, error) {
uc := &unstructuredConverter{
scheme: runtime.NewScheme(),
}
// Setup converter to understand custom CRD types
ambassador.AddToScheme(uc.scheme)
// Add the core types we need
if err := scheme.AddToScheme(uc.scheme); err != nil {
return nil, err
}
return uc, nil
}

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