From 44f319e6a058b5ac4f54ae139e229d337f77dd58 Mon Sep 17 00:00:00 2001 From: Toshikuni Fukaya Date: Wed, 25 Jul 2018 18:35:26 +0900 Subject: [PATCH 01/44] Support A record for multile IPs for a headless services. Non statefulset pods associating to a headless service have different IPs, but have a same hostname. In this case, external-dns registered only one A record due to attempting to register multiple A records for a same hostname for each IP. This patch now registers one A record having multiple IPs. --- source/service.go | 27 +++++++++++++++++---------- source/service_test.go | 38 ++++++++++++++++++-------------------- 2 files changed, 35 insertions(+), 30 deletions(-) diff --git a/source/service.go b/source/service.go index cba3ce685..8d7831d30 100644 --- a/source/service.go +++ b/source/service.go @@ -156,6 +156,7 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri return endpoints } + targetsByHeadlessDomain := make(map[string][]string) for _, v := range pods.Items { headlessDomain := hostname if v.Spec.Hostname != "" { @@ -166,11 +167,7 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri log.Debugf("Generating matching endpoint %s with HostIP %s", headlessDomain, v.Status.HostIP) // To reduce traffice on the DNS API only add record for running Pods. Good Idea? if v.Status.Phase == v1.PodRunning { - if ttl.IsConfigured() { - endpoints = append(endpoints, endpoint.NewEndpointWithTTL(headlessDomain, endpoint.RecordTypeA, ttl, v.Status.HostIP)) - } else { - endpoints = append(endpoints, endpoint.NewEndpoint(headlessDomain, endpoint.RecordTypeA, v.Status.HostIP)) - } + targetsByHeadlessDomain[headlessDomain] = append(targetsByHeadlessDomain[headlessDomain], v.Status.HostIP) } else { log.Debugf("Pod %s is not in running phase", v.Spec.Hostname) } @@ -178,11 +175,7 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri log.Debugf("Generating matching endpoint %s with PodIP %s", headlessDomain, v.Status.PodIP) // To reduce traffice on the DNS API only add record for running Pods. Good Idea? if v.Status.Phase == v1.PodRunning { - if ttl.IsConfigured() { - endpoints = append(endpoints, endpoint.NewEndpointWithTTL(headlessDomain, endpoint.RecordTypeA, ttl, v.Status.PodIP)) - } else { - endpoints = append(endpoints, endpoint.NewEndpoint(headlessDomain, endpoint.RecordTypeA, v.Status.PodIP)) - } + targetsByHeadlessDomain[headlessDomain] = append(targetsByHeadlessDomain[headlessDomain], v.Status.PodIP) } else { log.Debugf("Pod %s is not in running phase", v.Spec.Hostname) } @@ -190,6 +183,20 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri } + headlessDomains := []string{} + for headlessDomain := range targetsByHeadlessDomain { + headlessDomains = append(headlessDomains, headlessDomain) + } + sort.Strings(headlessDomains) + for _, headlessDomain := range headlessDomains { + targets := targetsByHeadlessDomain[headlessDomain] + if ttl.IsConfigured() { + endpoints = append(endpoints, endpoint.NewEndpointWithTTL(headlessDomain, endpoint.RecordTypeA, ttl, targets...)) + } else { + endpoints = append(endpoints, endpoint.NewEndpoint(headlessDomain, endpoint.RecordTypeA, targets...)) + } + } + return endpoints } diff --git a/source/service_test.go b/source/service_test.go index 2e6a59b1f..9989cd1e7 100644 --- a/source/service_test.go +++ b/source/service_test.go @@ -1235,7 +1235,7 @@ func TestHeadlessServices(t *testing.T) { labels map[string]string annotations map[string]string clusterIP string - podIP string + podIPs []string selector map[string]string lbs []string podnames []string @@ -1257,7 +1257,7 @@ func TestHeadlessServices(t *testing.T) { hostnameAnnotationKey: "service.example.org", }, v1.ClusterIPNone, - "1.1.1.1", + []string{"1.1.1.1", "1.1.1.2"}, map[string]string{ "component": "foo", }, @@ -1267,7 +1267,7 @@ func TestHeadlessServices(t *testing.T) { []v1.PodPhase{v1.PodRunning, v1.PodRunning}, []*endpoint.Endpoint{ {DNSName: "foo-0.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}}, - {DNSName: "foo-1.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}}, + {DNSName: "foo-1.service.example.org", Targets: endpoint.Targets{"1.1.1.2"}}, }, false, }, @@ -1285,7 +1285,7 @@ func TestHeadlessServices(t *testing.T) { ttlAnnotationKey: "1", }, v1.ClusterIPNone, - "1.1.1.1", + []string{"1.1.1.1", "1.1.1.2"}, map[string]string{ "component": "foo", }, @@ -1295,7 +1295,7 @@ func TestHeadlessServices(t *testing.T) { []v1.PodPhase{v1.PodRunning, v1.PodRunning}, []*endpoint.Endpoint{ {DNSName: "foo-0.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}, RecordTTL: endpoint.TTL(1)}, - {DNSName: "foo-1.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}, RecordTTL: endpoint.TTL(1)}, + {DNSName: "foo-1.service.example.org", Targets: endpoint.Targets{"1.1.1.2"}, RecordTTL: endpoint.TTL(1)}, }, false, }, @@ -1312,7 +1312,7 @@ func TestHeadlessServices(t *testing.T) { hostnameAnnotationKey: "service.example.org", }, v1.ClusterIPNone, - "1.1.1.1", + []string{"1.1.1.1", "1.1.1.2"}, map[string]string{ "component": "foo", }, @@ -1338,7 +1338,7 @@ func TestHeadlessServices(t *testing.T) { hostnameAnnotationKey: "service.example.org", }, v1.ClusterIPNone, - "1.1.1.1", + []string{"1.1.1.1", "1.1.1.2"}, map[string]string{ "component": "foo", }, @@ -1347,8 +1347,7 @@ func TestHeadlessServices(t *testing.T) { []string{"", ""}, []v1.PodPhase{v1.PodRunning, v1.PodRunning}, []*endpoint.Endpoint{ - {DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1"}}, - {DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1"}}, + {DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}}, }, false, }, @@ -1387,7 +1386,7 @@ func TestHeadlessServices(t *testing.T) { Annotations: tc.annotations, }, Status: v1.PodStatus{ - PodIP: tc.podIP, + PodIP: tc.podIPs[i], Phase: tc.phases[i], }, } @@ -1435,7 +1434,7 @@ func TestHeadlessServicesHostIP(t *testing.T) { labels map[string]string annotations map[string]string clusterIP string - hostIP string + hostIPs []string selector map[string]string lbs []string podnames []string @@ -1457,7 +1456,7 @@ func TestHeadlessServicesHostIP(t *testing.T) { hostnameAnnotationKey: "service.example.org", }, v1.ClusterIPNone, - "1.1.1.1", + []string{"1.1.1.1", "1.1.1.2"}, map[string]string{ "component": "foo", }, @@ -1467,7 +1466,7 @@ func TestHeadlessServicesHostIP(t *testing.T) { []v1.PodPhase{v1.PodRunning, v1.PodRunning}, []*endpoint.Endpoint{ {DNSName: "foo-0.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}}, - {DNSName: "foo-1.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}}, + {DNSName: "foo-1.service.example.org", Targets: endpoint.Targets{"1.1.1.2"}}, }, false, }, @@ -1485,7 +1484,7 @@ func TestHeadlessServicesHostIP(t *testing.T) { ttlAnnotationKey: "1", }, v1.ClusterIPNone, - "1.1.1.1", + []string{"1.1.1.1", "1.1.1.2"}, map[string]string{ "component": "foo", }, @@ -1495,7 +1494,7 @@ func TestHeadlessServicesHostIP(t *testing.T) { []v1.PodPhase{v1.PodRunning, v1.PodRunning}, []*endpoint.Endpoint{ {DNSName: "foo-0.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}, RecordTTL: endpoint.TTL(1)}, - {DNSName: "foo-1.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}, RecordTTL: endpoint.TTL(1)}, + {DNSName: "foo-1.service.example.org", Targets: endpoint.Targets{"1.1.1.2"}, RecordTTL: endpoint.TTL(1)}, }, false, }, @@ -1512,7 +1511,7 @@ func TestHeadlessServicesHostIP(t *testing.T) { hostnameAnnotationKey: "service.example.org", }, v1.ClusterIPNone, - "1.1.1.1", + []string{"1.1.1.1", "1.1.1.2"}, map[string]string{ "component": "foo", }, @@ -1538,7 +1537,7 @@ func TestHeadlessServicesHostIP(t *testing.T) { hostnameAnnotationKey: "service.example.org", }, v1.ClusterIPNone, - "1.1.1.1", + []string{"1.1.1.1", "1.1.1.2"}, map[string]string{ "component": "foo", }, @@ -1547,8 +1546,7 @@ func TestHeadlessServicesHostIP(t *testing.T) { []string{"", ""}, []v1.PodPhase{v1.PodRunning, v1.PodRunning}, []*endpoint.Endpoint{ - {DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1"}}, - {DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1"}}, + {DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}}, }, false, }, @@ -1587,7 +1585,7 @@ func TestHeadlessServicesHostIP(t *testing.T) { Annotations: tc.annotations, }, Status: v1.PodStatus{ - HostIP: tc.hostIP, + HostIP: tc.hostIPs[i], Phase: tc.phases[i], }, } From 59f0022b6daf54620fae475ace7199dd18bd9684 Mon Sep 17 00:00:00 2001 From: David Schneider Date: Fri, 16 Nov 2018 08:51:23 +0100 Subject: [PATCH 02/44] Change default apiversion of crd - Change default apiversion of DNSEndpoint - Add error to output CRDClient --- pkg/apis/externaldns/types.go | 2 +- pkg/apis/externaldns/types_test.go | 2 +- source/crd.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index f0637abe8..3e67a8bf9 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -162,7 +162,7 @@ var defaultConfig = &Config{ ExoscaleEndpoint: "https://api.exoscale.ch/dns", ExoscaleAPIKey: "", ExoscaleAPISecret: "", - CRDSourceAPIVersion: "externaldns.k8s.io/v1alpha", + CRDSourceAPIVersion: "externaldns.k8s.io/v1alpha1", CRDSourceKind: "DNSEndpoint", ServiceTypeFilter: []string{}, RFC2136Host: "", diff --git a/pkg/apis/externaldns/types_test.go b/pkg/apis/externaldns/types_test.go index e3094402b..87740640f 100644 --- a/pkg/apis/externaldns/types_test.go +++ b/pkg/apis/externaldns/types_test.go @@ -75,7 +75,7 @@ var ( ExoscaleEndpoint: "https://api.exoscale.ch/dns", ExoscaleAPIKey: "", ExoscaleAPISecret: "", - CRDSourceAPIVersion: "externaldns.k8s.io/v1alpha", + CRDSourceAPIVersion: "externaldns.k8s.io/v1alpha1", CRDSourceKind: "DNSEndpoint", } diff --git a/source/crd.go b/source/crd.go index 116166741..79b308ad4 100644 --- a/source/crd.go +++ b/source/crd.go @@ -69,7 +69,7 @@ func NewCRDClientForAPIVersionKind(client kubernetes.Interface, kubeConfig, kube } apiResourceList, err := client.Discovery().ServerResourcesForGroupVersion(groupVersion.String()) if err != nil { - return nil, nil, fmt.Errorf("error listing resources in GroupVersion %q", groupVersion.String()) + return nil, nil, fmt.Errorf("error listing resources in GroupVersion %q: %s", groupVersion.String(), err) } var crdAPIResource *metav1.APIResource From 1ed5fa6952430456bcf0efb6ce3724bfd6028f8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nick=20J=C3=BCttner?= Date: Tue, 20 Nov 2018 16:30:25 +0100 Subject: [PATCH 03/44] panic: assignment to entry in nil map --- provider/google.go | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/provider/google.go b/provider/google.go index 9339bf621..2958b175f 100644 --- a/provider/google.go +++ b/provider/google.go @@ -204,12 +204,9 @@ func (p *GoogleProvider) Records() (endpoints []*endpoint.Endpoint, _ error) { if !supportedRecordType(r.Type) { continue } - ep := &endpoint.Endpoint{ - DNSName: strings.TrimSuffix(r.Name, "."), - RecordType: r.Type, - Targets: make(endpoint.Targets, 0, len(r.Rrdatas)), - RecordTTL: endpoint.TTL(r.Ttl), - } + targets := make(endpoint.Targets, 0, len(r.Rrdatas)) + ep := endpoint.NewEndpointWithTTL(strings.TrimSuffix(r.Name, "."), r.Type, endpoint.TTL(r.Ttl), targets...) + for _, rr := range r.Rrdatas { // each page is processed sequentially, no need for a mutex here. ep.Targets = append(ep.Targets, strings.TrimSuffix(rr, ".")) From 919ad81ce77e2f0c5edc624d826114137ddc9195 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nick=20J=C3=BCttner?= Date: Wed, 21 Nov 2018 10:35:53 +0100 Subject: [PATCH 04/44] Remove trim suffix --- provider/google.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/provider/google.go b/provider/google.go index 2958b175f..0196475c9 100644 --- a/provider/google.go +++ b/provider/google.go @@ -205,14 +205,13 @@ func (p *GoogleProvider) Records() (endpoints []*endpoint.Endpoint, _ error) { continue } targets := make(endpoint.Targets, 0, len(r.Rrdatas)) - ep := endpoint.NewEndpointWithTTL(strings.TrimSuffix(r.Name, "."), r.Type, endpoint.TTL(r.Ttl), targets...) for _, rr := range r.Rrdatas { // each page is processed sequentially, no need for a mutex here. - ep.Targets = append(ep.Targets, strings.TrimSuffix(rr, ".")) + targets = append(targets, rr) } - sort.Sort(ep.Targets) - endpoints = append(endpoints, ep) + sort.Sort(targets) + endpoints = append(endpoints, endpoint.NewEndpointWithTTL(r.Name, r.Type, endpoint.TTL(r.Ttl), targets...)) } return nil From 7fdcf6b1079c50f4551652986b7bc248c767d38b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nick=20J=C3=BCttner?= Date: Wed, 21 Nov 2018 10:42:03 +0100 Subject: [PATCH 05/44] adjust gometalinter timeout by setting env var --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 166bcf585..c22f9041c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,6 +22,9 @@ before_install: install: - gometalinter --install +env: + - GOMETALINTER_DEADLINE="600s" + script: - vendor/github.com/kubernetes/repo-infra/verify/verify-boilerplate.sh --rootdir=$(pwd) - vendor/github.com/kubernetes/repo-infra/verify/verify-go-src.sh -v --rootdir $(pwd) From 4a330c61a3104ff62f794a21421853d47ce9fa3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nick=20J=C3=BCttner?= Date: Wed, 21 Nov 2018 12:13:04 +0100 Subject: [PATCH 06/44] Remove sorting of rrdatas --- provider/google.go | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/provider/google.go b/provider/google.go index 0196475c9..b3ddf44f8 100644 --- a/provider/google.go +++ b/provider/google.go @@ -204,14 +204,7 @@ func (p *GoogleProvider) Records() (endpoints []*endpoint.Endpoint, _ error) { if !supportedRecordType(r.Type) { continue } - targets := make(endpoint.Targets, 0, len(r.Rrdatas)) - - for _, rr := range r.Rrdatas { - // each page is processed sequentially, no need for a mutex here. - targets = append(targets, rr) - } - sort.Sort(targets) - endpoints = append(endpoints, endpoint.NewEndpointWithTTL(r.Name, r.Type, endpoint.TTL(r.Ttl), targets...)) + endpoints = append(endpoints, endpoint.NewEndpointWithTTL(r.Name, r.Type, endpoint.TTL(r.Ttl), r.Rrdatas...)) } return nil From bc0d80896cfd22e7bd6c7fc547195df9a0304333 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nick=20J=C3=BCttner?= Date: Wed, 21 Nov 2018 14:00:32 +0100 Subject: [PATCH 07/44] update dep dependencies --- Gopkg.lock | 5 +++-- Gopkg.toml | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index 7ae69152a..a692aa987 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -431,11 +431,12 @@ version = "1.1.4" [[projects]] - digest = "1:1c88ec29544b281964ed7a9a365b2802a523cd06c50cdee87eb3eec89cd864f4" + branch = "master" + digest = "1:fd50e2c52f29bb81f9a172f0d5aee1438b201ca0502ff3a20ebbe9629e274875" name = "github.com/kubernetes/repo-infra" packages = ["verify/boilerplate/test"] pruneopts = "" - revision = "c2f9667a4c29e70a39b0e89db2d4f0cab907dbee" + revision = "1bcb110c8726cee477939f507f4760a95e155347" [[projects]] digest = "1:7c23a751ce2f84663fa411acb87eae0da2d09c39a8e99b08bd8f65fae75d8928" diff --git a/Gopkg.toml b/Gopkg.toml index 328d11f42..2887deaab 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -54,7 +54,7 @@ ignored = ["github.com/kubernetes/repo-infra/kazel"] [[override]] name = "github.com/kubernetes/repo-infra" - revision = "c2f9667a4c29e70a39b0e89db2d4f0cab907dbee" + branch = "master" [[constraint]] name = "github.com/nesv/go-dynect" From 9cada6d9fe2ad0034ca88259d74d0af03961f7dd Mon Sep 17 00:00:00 2001 From: Martin Linkhorst Date: Wed, 21 Nov 2018 14:30:07 +0100 Subject: [PATCH 08/44] chore: remove unused import (#781) --- provider/google.go | 1 - 1 file changed, 1 deletion(-) diff --git a/provider/google.go b/provider/google.go index b3ddf44f8..0474a7446 100644 --- a/provider/google.go +++ b/provider/google.go @@ -18,7 +18,6 @@ package provider import ( "fmt" - "sort" "strings" "cloud.google.com/go/compute/metadata" From 7bbde7c4d45395636f30902df0aeeab2dfd97360 Mon Sep 17 00:00:00 2001 From: Martin Linkhorst Date: Wed, 21 Nov 2018 15:42:40 +0100 Subject: [PATCH 09/44] chore: update delivery.yaml to new format --- delivery.yaml | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/delivery.yaml b/delivery.yaml index 28ed23b5d..222ff01a5 100644 --- a/delivery.yaml +++ b/delivery.yaml @@ -1,19 +1,15 @@ -build_steps: -- desc: Install docker - cmd: | - apt-get update - apt-get install -y apt-transport-https ca-certificates curl software-properties-common - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - - add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" - apt-get update - apt-get install -y docker-ce -- desc: Build and push docker image - cmd: | - if [[ $CDP_TARGET_BRANCH == master && ! $CDP_PULL_REQUEST_NUMBER ]]; then - RELEASE_VERSION=$(git describe --tags --always --dirty) - IMAGE=registry-write.opensource.zalan.do/teapot/external-dns:${RELEASE_VERSION} - else - IMAGE=registry-write.opensource.zalan.do/teapot/external-dns-test:${CDP_BUILD_VERSION} - fi - docker build --squash --tag "$IMAGE" . - docker push "$IMAGE" +version: "2017-09-20" +pipeline: +- id: build + type: script + commands: + - desc: Build and push Docker image + cmd: | + if [[ $CDP_TARGET_BRANCH == master && ! $CDP_PULL_REQUEST_NUMBER ]]; then + RELEASE_VERSION=$(git describe --tags --always --dirty) + IMAGE=registry-write.opensource.zalan.do/teapot/external-dns:${RELEASE_VERSION} + else + IMAGE=registry-write.opensource.zalan.do/teapot/external-dns-test:${CDP_BUILD_VERSION} + fi + docker build --squash --tag "$IMAGE" . + docker push "$IMAGE" From 63dee0485b1eb90893b1d91e74e02d28be5f0b7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nick=20J=C3=BCttner?= Date: Thu, 22 Nov 2018 13:51:38 +0100 Subject: [PATCH 10/44] Changelog v0.5.9 --- CHANGELOG.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 376d41b90..5789b6b13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,28 @@ +## 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 From 73fdde069bac14dd6772d7c77f74b17f22797b77 Mon Sep 17 00:00:00 2001 From: David Schneider Date: Fri, 23 Nov 2018 13:48:26 +0100 Subject: [PATCH 11/44] Improve errors in Records() of infoblox provider --- provider/infoblox.go | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/provider/infoblox.go b/provider/infoblox.go index 0d93a8342..3136d3557 100644 --- a/provider/infoblox.go +++ b/provider/infoblox.go @@ -17,6 +17,7 @@ limitations under the License. package provider import ( + "fmt" "os" "strconv" "strings" @@ -25,6 +26,7 @@ import ( "github.com/kubernetes-incubator/external-dns/endpoint" "github.com/kubernetes-incubator/external-dns/plan" "github.com/sirupsen/logrus" + log "github.com/sirupsen/logrus" ) // InfobloxConfig clarifies the method signature @@ -95,10 +97,11 @@ func NewInfobloxProvider(infobloxConfig InfobloxConfig) (*InfobloxProvider, erro func (p *InfobloxProvider) Records() (endpoints []*endpoint.Endpoint, err error) { zones, err := p.zones() if err != nil { - return nil, err + return nil, fmt.Errorf("could not fetch zones: %s", err) } for _, zone := range zones { + log.Debugf("fetch records from zone '%s'", zone.Fqdn) var resA []ibclient.RecordA objA := ibclient.NewRecordA( ibclient.RecordA{ @@ -107,7 +110,7 @@ func (p *InfobloxProvider) Records() (endpoints []*endpoint.Endpoint, err error) ) err = p.client.GetObject(objA, "", &resA) if err != nil { - return nil, err + 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)) @@ -122,7 +125,7 @@ func (p *InfobloxProvider) Records() (endpoints []*endpoint.Endpoint, err error) ) err = p.client.GetObject(objH, "", &resH) if err != nil { - return nil, err + return nil, fmt.Errorf("could not fetch host records from zone '%s': %s", zone.Fqdn, err) } for _, res := range resH { for _, ip := range res.Ipv4Addrs { @@ -138,7 +141,7 @@ func (p *InfobloxProvider) Records() (endpoints []*endpoint.Endpoint, err error) ) err = p.client.GetObject(objC, "", &resC) if err != nil { - return nil, err + return nil, fmt.Errorf("could not fetch CNAME records from zone '%s': %s", zone.Fqdn, err) } for _, res := range resC { endpoints = append(endpoints, endpoint.NewEndpoint(res.Name, endpoint.RecordTypeCNAME, res.Canonical)) @@ -152,7 +155,7 @@ func (p *InfobloxProvider) Records() (endpoints []*endpoint.Endpoint, err error) ) err = p.client.GetObject(objT, "", &resT) if err != nil { - return nil, err + return nil, fmt.Errorf("could not fetch TXT records from zone '%s': %s", zone.Fqdn, err) } for _, res := range resT { // The Infoblox API strips enclosing double quotes from TXT records lacking whitespace. @@ -163,6 +166,7 @@ func (p *InfobloxProvider) Records() (endpoints []*endpoint.Endpoint, err error) endpoints = append(endpoints, endpoint.NewEndpoint(res.Name, endpoint.RecordTypeTXT, res.Text)) } } + log.Debugf("fetched %d records from infoblox", len(endpoints)) return endpoints, nil } From 5bbaf7f3fc4974830054c514b27ae51a3b09dfa7 Mon Sep 17 00:00:00 2001 From: Author pelithne Date: Fri, 23 Nov 2018 15:37:56 +0100 Subject: [PATCH 12/44] Updating Azure tutorial --- docs/tutorials/azure.md | 62 +++++++++++++---------------------------- 1 file changed, 19 insertions(+), 43 deletions(-) diff --git a/docs/tutorials/azure.md b/docs/tutorials/azure.md index 7d1a59515..42778721a 100644 --- a/docs/tutorials/azure.md +++ b/docs/tutorials/azure.md @@ -9,7 +9,7 @@ This tutorial uses [Azure CLI 2.0](https://docs.microsoft.com/en-us/cli/azure/in Azure commands and assumes that the Kubernetes cluster was created via Azure Container Services and `kubectl` commands are being run on an orchestration master. -## Creating a Azure DNS zone +## Creating an Azure DNS zone The Azure provider for ExternalDNS will find suitable zones for domains it manages; it will not automatically create zones. @@ -34,62 +34,33 @@ If using your own domain that was registered with a third-party domain registrar name servers to the values in the `nameServers` field from the JSON data returned by the `az network dns zone create` command. Please consult your registrar's documentation on how to do that. -## Creating Azure Credentials Secret -The Azure DNS provider expects, by default, that the configuration file is at `/etc/kubernetes/azure.json`. This can be overridden with -the `--azure-config-file` option when starting ExternalDNS. +## Permissions to modify DNS zone +External-DNS needs permissions to make changes in the Azure DNS server. These permissions are defined in a Service Principal that should be made available to External-DNS as a configuration file. -### Use provisioned VM configuration file -When running within Azure (ACS or AKS), the agent and master VMs are already provisioned with the configuration file at `/etc/kubernetes/azure.json`. +The Azure DNS provider expects, by default, that the configuration file is at `/etc/kubernetes/azure.json`. This can be overridden with the `--azure-config-file` option when starting ExternalDNS. -If you want to use the file directly, make sure that the service principal that is given there has access to contribute to the resource group containing the Azure DNS zone(s). - -To use the file, replace the directive - -```yaml - volumes: - - name: azure-config-file - secret: - secretName: azure-config-file -``` - -with - -```yaml - volumes: - - name: azure-config-file - hostPath: - path: /etc/kubernetes/azure.json - type: File -``` - -in the manifests below. - -### Use custom configuration file -If you want to customize the configuration, for example because you want to use a different service principal, you have to manually create a secret. -This is also required if the Kubernetes cluster is not hosted in Azure Container Services (ACS or AKS) and you still want to use Azure DNS. - -The secret should contain an object named azure.json with content similar to this: +### Creating configuration file +The preferred way to inject the configuration file is by using a Kubernetes secret. The secret should contain an object named azure.json with content similar to this: ```json { "tenantId": "01234abc-de56-ff78-abc1-234567890def", "subscriptionId": "01234abc-de56-ff78-abc1-234567890def", + "resourceGroup": "MyDnsResourceGroup", "aadClientId": "01234abc-de56-ff78-abc1-234567890def", "aadClientSecret": "uKiuXeiwui4jo9quae9o", - "resourceGroup": "MyDnsResourceGroup", } ``` -You can find the `tenantId` by running `az account show` or by selecting Azure Active Directory in the Azure Portal and checking the _Directory ID_ under Properties. +You can find the `tenantId` by running `az account show --query "tenantId"` or by selecting Azure Active Directory in the Azure Portal and checking the _Directory ID_ under Properties. + You can find the `subscriptionId` by running `az account show --query "id"` or by selecting Subscriptions in the Azure Portal. -To create the secret: +The `resourceGroup` is the Resource Group created in a previous step. -``` -$ kubectl create secret generic azure-config-file --from-file=/local/path/to/azure.json -``` +The `aadClientID` and `aaClientSecret` are assoiated with the Service Principal, that you need to create next. -#### Create service principal +### Creating service principal A Service Principal with a minimum access level of contribute to the resource group containing the Azure DNS zone(s) is necessary for ExternalDNS to be able to edit DNS records. This is an Azure CLI example on how to query the Azure API for the information required for the Resource Group and DNS zone you would have already created in previous steps. ``` @@ -126,7 +97,13 @@ A Service Principal with a minimum access level of contribute to the resource gr } ``` -#### Azure Managed Service Identity (MSI) +Now you can create a file named 'azure.json' with values gathered above and with the structure of the example above. Use this file to create a Kubernetes secret: + +``` +$ kubectl create secret generic azure-config-file --from-file=/local/path/to/azure.json +``` + +### Azure Managed Service Identity (MSI) If [Azure Managed Service Identity (MSI)](https://docs.microsoft.com/en-us/azure/active-directory/managed-service-identity/overview) is enabled for virtual machines, then there is no need to create separate service principal. @@ -149,7 +126,6 @@ kubectl create secret generic azure-config-file --from-file=azure.json ``` - ## Deploy ExternalDNS This deployment assumes that you will be using nginx-ingress. When using nginx-ingress do not deploy it as a Daemon Set. This causes nginx-ingress to write the Cluster IP of the backend pods in the ingress status.loadbalancer.ip property which then has external-dns write the Cluster IP(s) in DNS vs. the nginx-ingress service external IP. From 4e0d5c1cfff14b88366051eec42817b68a4ad6b4 Mon Sep 17 00:00:00 2001 From: Marques Johansson Date: Sat, 24 Nov 2018 14:50:38 -0500 Subject: [PATCH 13/44] update README to include Linode on the 0.5 roadmap (#787) Notes that Linode support was added in 0.5.5 --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a411ac638..0b6128adc 100644 --- a/README.md +++ b/README.md @@ -178,6 +178,7 @@ Here's a rough outline on what is to come (subject to change): - [x] Support for creating DNS records to multiple targets (for Google and AWS) - [x] Support for OpenStack Designate - [x] Support for PowerDNS +- [x] Support for Linode ### v0.6 From 31e50b792cddbca65d20f24dd5d823399c3cb8a6 Mon Sep 17 00:00:00 2001 From: xunpan <42633647+xunpan@users.noreply.github.com> Date: Wed, 28 Nov 2018 17:59:24 +0800 Subject: [PATCH 14/44] add tutorial for coredns (#791) There is no coredns tutorial for externalDNS. This pull request makes coredns based on minikube for working with externalDNS. --- README.md | 1 + docs/tutorials/coredns.md | 164 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 docs/tutorials/coredns.md diff --git a/README.md b/README.md index 0b6128adc..25c2b5536 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ The following tutorials are provided: * [AWS (Route53)](docs/tutorials/aws.md) * [AWS (Service Discovery)](docs/tutorials/aws-sd.md) * [Azure](docs/tutorials/azure.md) +* [CoreDNS](docs/tutorials/coredns.md) * [Cloudflare](docs/tutorials/cloudflare.md) * [DigitalOcean](docs/tutorials/digitalocean.md) * [Infoblox](docs/tutorials/infoblox.md) diff --git a/docs/tutorials/coredns.md b/docs/tutorials/coredns.md new file mode 100644 index 000000000..12a357dc0 --- /dev/null +++ b/docs/tutorials/coredns.md @@ -0,0 +1,164 @@ +# 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). +You need to: +* install CoreDNS with [etcd](https://github.com/etcd-io/etcd) enabled +* install external-dns with coredns as a provider +* enable ingress controller for the minikube cluster + + +## Creating a cluster +``` +minikube start +``` + +## Installing CoreDNS with etcd enabled +Helm chart is used to install etcd and CoreDNS. +### Initializing helm chart +``` +helm init +``` +### Installing etcd +[etcd operator](https://github.com/coreos/etcd-operator) is used to manage etcd clusters. +``` +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/master/example/example-etcd-cluster.yaml +``` + +### Installing CoreDNS +In order to make CoreDNS work with etcd backend, values.yaml of the chart should be changed with corresponding configurations. +``` +wget https://raw.githubusercontent.com/helm/charts/master/stable/coredns/values.yaml +``` + +You need to edit/patch the file with below diff +``` +diff --git a/values.yaml b/values.yaml +index 964e72b..e2fa934 100644 +--- a/values.yaml ++++ b/values.yaml +@@ -27,12 +27,12 @@ service: + + rbac: + # If true, create & use RBAC resources +- create: false ++ create: true + # Ignored if rbac.create is true + serviceAccountName: default + + # isClusterService specifies whether chart should be deployed as cluster-service or normal k8s app. +-isClusterService: true ++isClusterService: false + + servers: + - zones: +@@ -51,6 +51,12 @@ servers: + parameters: 0.0.0.0:9153 + - name: proxy + parameters: . /etc/resolv.conf ++ - name: etcd ++ parameters: example.org ++ configBlock: |- ++ stubzones ++ path /skydns ++ endpoint http://10.105.68.165:2379 + + # Complete example with all the options: + # - zones: # the `zones` block can be left out entirely, defaults to "." +``` +**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 +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +example-etcd-cluster-client ClusterIP 10.105.68.165 2379/TCP 16m +``` +* Parameters should configure your own domain. "example.org" is used in this example. + + +After configuration done in values.yaml, you can install coredns chart. +``` +helm install --name my-coredns --values values.yaml stable/coredns +``` + +## Installing ExternalDNS +### Install external ExternalDNS +ETCD_URLS is configured to etcd client service address. +``` +$ cat external-dns.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: external-dns + namespace: kube-system +spec: + strategy: + type: Recreate + selector: + matchLabels: + app: external-dns + template: + metadata: + labels: + app: external-dns + spec: + containers: + - name: external-dns + image: registry.opensource.zalan.do/teapot/external-dns:latest + args: + - --source=ingress + - --provider=coredns + - --log-level=debug # debug only + env: + - name: ETCD_URLS + value: http://10.105.68.165:2379 +$ kubectl apply -f external-dns.yaml +``` + +## Enable the ingress controller +You can use the ingress controller in minikube cluster. It needs to enable ingress addon in the cluster. +``` +minikube addons enable ingress +``` + +## Testing ingress example +``` +$ cat ingress.yaml +kind: Ingress +metadata: + name: nginx + annotations: + kubernetes.io/ingress.class: "nginx" +spec: + rules: + - host: nginx.example.org + http: + paths: + - backend: + serviceName: nginx + servicePort: 80 + +$ kubectl apply -f ingress.yaml +ingress.extensions "nginx" created +``` + + +Wait a moment until DNS has the ingress IP. The DNS service IP is from CoreDNS service. It is "my-coredns-coredns" in this example. +``` +$ kubectl get svc my-coredns-coredns +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +my-coredns-coredns ClusterIP 10.100.4.143 53/UDP 12m + +$ kubectl get ingress +NAME HOSTS ADDRESS PORTS AGE +nginx nginx.example.org 10.0.2.15 80 2m + +$ 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.102.213.122 nginx.example.org +short +dnstools# dig @10.102.213.122 nginx.example.org +short +10.0.2.15 +dnstools# +``` From 84497bfad7a489e71b26d5992da0367add66f8b3 Mon Sep 17 00:00:00 2001 From: Martin Linkhorst Date: Wed, 28 Nov 2018 11:18:48 +0100 Subject: [PATCH 15/44] fix(infoblox): don't import logrus twice --- provider/infoblox.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/provider/infoblox.go b/provider/infoblox.go index 3136d3557..71c89eefe 100644 --- a/provider/infoblox.go +++ b/provider/infoblox.go @@ -26,7 +26,6 @@ import ( "github.com/kubernetes-incubator/external-dns/endpoint" "github.com/kubernetes-incubator/external-dns/plan" "github.com/sirupsen/logrus" - log "github.com/sirupsen/logrus" ) // InfobloxConfig clarifies the method signature @@ -101,7 +100,7 @@ func (p *InfobloxProvider) Records() (endpoints []*endpoint.Endpoint, err error) } for _, zone := range zones { - log.Debugf("fetch records from zone '%s'", zone.Fqdn) + logrus.Debugf("fetch records from zone '%s'", zone.Fqdn) var resA []ibclient.RecordA objA := ibclient.NewRecordA( ibclient.RecordA{ @@ -166,7 +165,7 @@ func (p *InfobloxProvider) Records() (endpoints []*endpoint.Endpoint, err error) endpoints = append(endpoints, endpoint.NewEndpoint(res.Name, endpoint.RecordTypeTXT, res.Text)) } } - log.Debugf("fetched %d records from infoblox", len(endpoints)) + logrus.Debugf("fetched %d records from infoblox", len(endpoints)) return endpoints, nil } From 398b49b3d46afe429644cb0b2d9c3439e5529ece Mon Sep 17 00:00:00 2001 From: Martin Linkhorst Date: Wed, 28 Nov 2018 15:02:05 +0100 Subject: [PATCH 16/44] feat(controller): expose managed resources and records as metrics --- controller/controller.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/controller/controller.go b/controller/controller.go index fec621bc8..769c6553a 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -40,11 +40,29 @@ var ( Help: "Number of Source errors.", }, ) + sourceEndpointsTotal = prometheus.NewGauge( + prometheus.GaugeOpts{ + Namespace: "external_dns", + Subsystem: "source", + Name: "endpoints_total", + Help: "Number of Endpoints in all sources", + }, + ) + registryEndpointsTotal = prometheus.NewGauge( + prometheus.GaugeOpts{ + Namespace: "external_dns", + Subsystem: "registry", + Name: "endpoints_total", + Help: "Number of Endpoints in the registry", + }, + ) ) func init() { prometheus.MustRegister(registryErrors) prometheus.MustRegister(sourceErrors) + prometheus.MustRegister(sourceEndpointsTotal) + prometheus.MustRegister(registryEndpointsTotal) } // Controller is responsible for orchestrating the different components. @@ -69,12 +87,14 @@ func (c *Controller) RunOnce() error { registryErrors.Inc() return err } + registryEndpointsTotal.Set(float64(len(records))) endpoints, err := c.Source.Endpoints() if err != nil { sourceErrors.Inc() return err } + sourceEndpointsTotal.Set(float64(len(endpoints))) plan := &plan.Plan{ Policies: []plan.Policy{c.Policy}, From 78c63c3187d6fdb95803a7eae0a272d7d4f88463 Mon Sep 17 00:00:00 2001 From: Marques Johansson Date: Fri, 30 Nov 2018 14:56:51 -0500 Subject: [PATCH 17/44] update the FAQ list of supported DNS providers (#796) --- docs/faq.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/faq.md b/docs/faq.md index d5a1666ab..927a5b9a4 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -40,6 +40,11 @@ Currently, the following providers are supported: - Dyn - OpenStack Designate - PowerDNS +- CoreDNS +- Exoscale +- Oracle Cloud Infrastructure DNS +- Linode DNS +- RFC2136 As stated in the README, we are currently looking for stable maintainers for those providers, to ensure that bugfixes and new features will be available for all of those. From f25f90db0ed66910ab60416fc8d26b9159e83d9c Mon Sep 17 00:00:00 2001 From: Davis Phillips Date: Fri, 30 Nov 2018 13:57:06 -0600 Subject: [PATCH 18/44] adding config for bind for tsig (#790) * adding config for bind for tsig * add indentation as requested --- docs/tutorials/rfc2136.md | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/docs/tutorials/rfc2136.md b/docs/tutorials/rfc2136.md index 5b0bf7304..1ff0f6160 100644 --- a/docs/tutorials/rfc2136.md +++ b/docs/tutorials/rfc2136.md @@ -12,6 +12,39 @@ key "externaldns-key" { ``` - `Warning!` Bind server configuration should enable for this key AFXR zone transfer protocol. It is used for listing DNS records. +```text +# cat /etc/named.conf +... +include "/etc/rndc.key"; + +controls { + inet 123.123.123.123 port 953 allow { 10.x.y.151; } keys { "externaldns-key"; }; +}; +options { + include "/etc/named/options.conf"; +}; + +include "/etc/named/zones.conf"; +... + +# cat /etc/named/options.conf +... +dnssec-enable yes; +dnssec-validation yes; +... + +# cat /etc/named/zones.conf +... +zone "example.com" { + type master; + file "/var/named/dynamic/db.example.com"; + update-policy { + grant externaldns-key zonesub ANY; + }; +}; +... +``` + ## RFC2136 provider configuration: - Example fragment of real configuration of ExternalDNS service pod. @@ -31,4 +64,4 @@ key "externaldns-key" { - `rfc2136-tsig-keyname` - this is string parameter with secret key name it is should `MATCH!` with server key name. In example it is `externaldns-key`. - \ No newline at end of file + From 7b3c5dd8ad63a4ae19df20008fbb7fc88c1dc674 Mon Sep 17 00:00:00 2001 From: Sanyu Melwani <1154057+sanyu@users.noreply.github.com> Date: Tue, 4 Dec 2018 12:22:02 +1100 Subject: [PATCH 19/44] Use SOAP API to retrieve all records with 1 request --- Gopkg.lock | 9 ++ Gopkg.toml | 4 + provider/dyn.go | 192 ++++++++++++++++++++++++------------------- provider/dyn_test.go | 37 --------- 4 files changed, 119 insertions(+), 123 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index a692aa987..d5b1ec160 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -585,6 +585,14 @@ pruneopts = "" revision = "a6e9df898b1336106c743392c48ee0b71f5c4efa" +[[projects]] + branch = "master" + digest = "1:bb2ccb2d56cbafdec58af0f473f45304e19876f09fa671960ca87802b656a9c0" + name = "github.com/sanyu/dynectsoap" + packages = ["dynectsoap"] + pruneopts = "" + revision = "b83de5edc4e022f22903eeb3b428d2f39fb740e5" + [[projects]] digest = "1:7f569d906bdd20d906b606415b7d794f798f91a62fcfb6a4daa6d50690fb7a3f" name = "github.com/satori/go.uuid" @@ -1149,6 +1157,7 @@ "github.com/pkg/errors", "github.com/prometheus/client_golang/prometheus", "github.com/prometheus/client_golang/prometheus/promhttp", + "github.com/sanyu/dynectsoap/dynectsoap", "github.com/sirupsen/logrus", "github.com/stretchr/testify/assert", "github.com/stretchr/testify/mock", diff --git a/Gopkg.toml b/Gopkg.toml index 2887deaab..17cc1e3cc 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -115,3 +115,7 @@ ignored = ["github.com/kubernetes/repo-infra/kazel"] [[constraint]] name = "github.com/miekg/dns" version = "1.0.8" + +[[constraint]] + name = "github.com/sanyu/dynectsoap" + branch = "master" diff --git a/provider/dyn.go b/provider/dyn.go index 36eefdbfb..fd0280250 100644 --- a/provider/dyn.go +++ b/provider/dyn.go @@ -26,6 +26,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/nesv/go-dynect/dynect" + "github.com/sanyu/dynectsoap/dynectsoap" "github.com/kubernetes-incubator/external-dns/endpoint" "github.com/kubernetes-incubator/external-dns/plan" @@ -34,9 +35,6 @@ import ( const ( // 10 minutes default timeout if not configured using flags dynDefaultTTL = 600 - // can store 20000 entries globally, that's about 4MB of memory - // may be made configurable in the future but 20K records seems like enough for a few zones - cacheMaxSize = 20000 // when rate limit is hit retry up to 5 times after sleep 1m between retries dynMaxRetriesOnErrRateLimited = 5 @@ -51,50 +49,10 @@ const ( restAPIPrefix = "/REST/" ) -// A simple non-thread-safe cache with TTL. The TTL of the records is used here to -// This cache is used to save on requests to DynAPI -type cache struct { - contents map[string]*entry -} - -type entry struct { - expires int64 - ep *endpoint.Endpoint -} - -func (c *cache) Put(link string, ep *endpoint.Endpoint) { - // flush the whole cache on overflow - if len(c.contents) >= cacheMaxSize { - log.Debugf("Flushing cache") - c.contents = make(map[string]*entry) - } - - c.contents[link] = &entry{ - ep: ep, - expires: unixNow() + int64(ep.RecordTTL), - } -} - func unixNow() int64 { return int64(time.Now().Unix()) } -func (c *cache) Get(link string) *endpoint.Endpoint { - result, ok := c.contents[link] - if !ok { - return nil - } - - now := unixNow() - - if result.expires < now { - delete(c.contents, link) - return nil - } - - return result.ep -} - // DynConfig hold connection parameters to dyn.com and internal state type DynConfig struct { DomainFilter DomainFilter @@ -145,7 +103,6 @@ func (snap *ZoneSnapshot) StoreRecordsForSerial(zone string, serial int, records // DynProvider is the actual interface impl. type dynProviderState struct { DynConfig - Cache *cache LastLoginErrorTime int64 ZoneSnapshot *ZoneSnapshot @@ -186,9 +143,6 @@ type ZonePublishResponse struct { func NewDynProvider(config DynConfig) (Provider, error) { return &dynProviderState{ DynConfig: config, - Cache: &cache{ - contents: make(map[string]*entry), - }, ZoneSnapshot: &ZoneSnapshot{ endpoints: map[string][]*endpoint.Endpoint{}, serials: map[string]int{}, @@ -315,40 +269,48 @@ func apiRetryLoop(f func() error) error { return err } -// recordLinkToEndpoint makes an Endpoint given a resource link optinally making a remote call if a cached entry is expired -func (d *dynProviderState) recordLinkToEndpoint(client *dynect.Client, recordLink string) (*endpoint.Endpoint, error) { - result := d.Cache.Get(recordLink) - if result != nil { - log.Infof("Using cached endpoint for %s: %+v", recordLink, result) - return result, nil +func (d *dynProviderState) allRecordsToEndpoints(records *dynectsoap.GetAllRecordsResponseType) []*endpoint.Endpoint { + result := []*endpoint.Endpoint{} + //Convert each record to an endpoint + + //Process A Records + for _, rec := range records.Data.A_records { + ep := &endpoint.Endpoint{ + DNSName: rec.Fqdn, + RecordTTL: endpoint.TTL(rec.Ttl), + RecordType: rec.Record_type, + Targets: endpoint.Targets{rec.Rdata.Address}, + } + log.Debugf("A record: %v", *ep) + result = append(result, ep) } - rec := dynect.RecordResponse{} - - err := apiRetryLoop(func() error { - return client.Do("GET", recordLink, nil, &rec) - }) - - if err != nil { - return nil, err + //Process CNAME Records + for _, rec := range records.Data.Cname_records { + ep := &endpoint.Endpoint{ + DNSName: rec.Fqdn, + RecordTTL: endpoint.TTL(rec.Ttl), + RecordType: rec.Record_type, + Targets: endpoint.Targets{strings.TrimSuffix(rec.Rdata.Cname, ".")}, + } + log.Debugf("CNAME record: %v", *ep) + result = append(result, ep) } - // ignore all records but the types supported by external- - target := extractTarget(rec.Data.RecordType, &rec.Data.RData) - if target == "" { - return nil, nil + //Process TXT Records + for _, rec := range records.Data.Txt_records { + ep := &endpoint.Endpoint{ + DNSName: rec.Fqdn, + RecordTTL: endpoint.TTL(rec.Ttl), + RecordType: rec.Record_type, + Targets: endpoint.Targets{rec.Rdata.Txtdata}, + } + log.Debugf("TXT record: %v", *ep) + result = append(result, ep) } - result = &endpoint.Endpoint{ - DNSName: rec.Data.FQDN, - RecordTTL: endpoint.TTL(rec.Data.TTL), - RecordType: rec.Data.RecordType, - Targets: endpoint.Targets{target}, - } + return result - log.Debugf("Fetched new endpoint for %s: %+v", recordLink, result) - d.Cache.Put(recordLink, result) - return result, nil } func errorOrValue(err error, value interface{}) interface{} { @@ -387,6 +349,72 @@ func (d *dynProviderState) fetchZoneSerial(client *dynect.Client, zone string) ( return resp.Data.Serial, nil } +//Use SOAP to fetch all records with a single call +func (d *dynProviderState) fetchAllRecordsInZone(zone string) (*dynectsoap.GetAllRecordsResponseType, error) { + var err error + client := dynectsoap.NewClient("https://api2.dynect.net/SOAP/") + service := dynectsoap.NewDynect(client) + + sessionRequest := dynectsoap.SessionLoginRequestType{ + Customer_name: d.CustomerName, + User_name: d.Username, + Password: d.Password, + Fault_incompat: 0, + } + resp := dynectsoap.SessionLoginResponseType{} + err = apiRetryLoop(func() error { + return service.Do(&sessionRequest, &resp) + }) + + if err != nil { + return nil, err + } + + token := resp.Data.Token + + logoutRequest := dynectsoap.SessionLogoutRequestType{ + Token: token, + Fault_incompat: 0, + } + logoutResponse := dynectsoap.SessionLogoutResponseType{} + defer service.Do(&logoutRequest, &logoutResponse) + + req := dynectsoap.GetAllRecordsRequestType{ + Token: token, + Zone: zone, + Fault_incompat: 0, + } + records := dynectsoap.GetAllRecordsResponseType{} + err = apiRetryLoop(func() error { + return service.Do(&req, &records) + }) + + if err != nil { + return nil, err + } + log.Debugf("Got all Records, status is %s", records.Status) + + if strings.ToLower(records.Status) == "incomplete" { + jobRequest := dynectsoap.GetJobRequestType{ + Token: token, + Job_id: records.Job_id, + Fault_incompat: 0, + } + + jobResults := dynectsoap.GetJobResponseType{} + err = apiRetryLoop(func() error { + return service.GetJobRetry(&jobRequest, &jobResults) + }) + if err != nil { + return nil, err + } + return jobResults.Data.(*dynectsoap.GetAllRecordsResponseType), nil + } + + return &records, nil + +} + // fetchAllRecordLinksInZone list all records in a zone with a single call. Records not matched by the // DomainFilter are ignored. The response is a list of links that can be fed to dynect.Client.Do() // directly @@ -611,22 +639,14 @@ func (d *dynProviderState) Records() ([]*endpoint.Endpoint, error) { continue } - recordLinks, err := d.fetchAllRecordLinksInZone(client, zone) + //Fetch All Records + records, err := d.fetchAllRecordsInZone(zone) if err != nil { return nil, err } + relevantRecords = d.allRecordsToEndpoints(records) - log.Infof("Found %d relevant records found in zone %s: %+v", len(recordLinks), zone, recordLinks) - for _, link := range recordLinks { - ep, err := d.recordLinkToEndpoint(client, link) - if err != nil { - return nil, err - } - - if ep != nil { - relevantRecords = append(relevantRecords, ep) - } - } + log.Debugf("Relevant records %+v", relevantRecords) d.ZoneSnapshot.StoreRecordsForSerial(zone, serial, relevantRecords) log.Infof("Stored %d records for %s@%d", len(relevantRecords), zone, serial) diff --git a/provider/dyn_test.go b/provider/dyn_test.go index 4beda817a..e93bbd5ce 100644 --- a/provider/dyn_test.go +++ b/provider/dyn_test.go @@ -20,7 +20,6 @@ import ( "errors" "fmt" "testing" - "time" "github.com/nesv/go-dynect/dynect" "github.com/stretchr/testify/assert" @@ -264,42 +263,6 @@ func TestDyn_fixMissingTTL(t *testing.T) { assert.Equal(t, "1992", fixMissingTTL(endpoint.TTL(111), 1992)) } -func TestDyn_cachePut(t *testing.T) { - c := cache{ - contents: make(map[string]*entry), - } - - c.Put("link", &endpoint.Endpoint{ - DNSName: "name", - Targets: endpoint.Targets{"target"}, - RecordTTL: endpoint.TTL(10000), - RecordType: "A", - }) - - found := c.Get("link") - assert.NotNil(t, found) -} - -func TestDyn_cachePutExpired(t *testing.T) { - c := cache{ - contents: make(map[string]*entry), - } - - c.Put("link", &endpoint.Endpoint{ - DNSName: "name", - Targets: endpoint.Targets{"target"}, - RecordTTL: endpoint.TTL(0), - RecordType: "A", - }) - - time.Sleep(2 * time.Second) - - found := c.Get("link") - assert.Nil(t, found) - - assert.Nil(t, c.Get("no-such-records")) -} - func TestDyn_Snapshot(t *testing.T) { snap := ZoneSnapshot{ serials: map[string]int{}, From 36b443f853690737775f88fec7d41e09fe83879d Mon Sep 17 00:00:00 2001 From: Matteo Dell'Aquila Date: Tue, 4 Dec 2018 16:02:32 +0100 Subject: [PATCH 20/44] fix json syntax error - typing error (#765) there was an unexpected comma in json used as custom configuration file --- docs/tutorials/azure.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/azure.md b/docs/tutorials/azure.md index 42778721a..cdd45e7f6 100644 --- a/docs/tutorials/azure.md +++ b/docs/tutorials/azure.md @@ -48,7 +48,7 @@ The preferred way to inject the configuration file is by using a Kubernetes secr "subscriptionId": "01234abc-de56-ff78-abc1-234567890def", "resourceGroup": "MyDnsResourceGroup", "aadClientId": "01234abc-de56-ff78-abc1-234567890def", - "aadClientSecret": "uKiuXeiwui4jo9quae9o", + "aadClientSecret": "uKiuXeiwui4jo9quae9o" } ``` From 159d10983338448bc5851d1caca5a4b98ec03dce Mon Sep 17 00:00:00 2001 From: xunpan Date: Fri, 7 Dec 2018 02:45:47 -0500 Subject: [PATCH 21/44] 2 issues: - coredns support more than 1 targets - delete with prefix to make sure the record is cleaned --- provider/coredns.go | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/provider/coredns.go b/provider/coredns.go index 38403ae61..4e78bdbf4 100644 --- a/provider/coredns.go +++ b/provider/coredns.go @@ -153,7 +153,7 @@ func (c etcdClient) DeleteService(key string) error { ctx, cancel := context.WithTimeout(c.ctx, etcdTimeout) defer cancel() - _, err := c.client.Delete(ctx, key) + _, err := c.client.Delete(ctx, key, etcdcv3.WithPrefix()) return err } @@ -317,22 +317,26 @@ func (p coreDNSProvider) ApplyChanges(changes *plan.Changes) error { if ep.RecordType == endpoint.RecordTypeTXT { continue } - prefix := ep.Labels[randomPrefixLabel] - if prefix == "" { - prefix = fmt.Sprintf("%08x", rand.Int31()) + + for _, target := range ep.Targets { + prefix := ep.Labels[randomPrefixLabel] + if prefix == "" { + prefix = fmt.Sprintf("%08x", rand.Int31()) + } + + service := Service{ + Host: target, + Text: ep.Labels["originalText"], + Key: etcdKeyFor(prefix + "." + dnsName), + TargetStrip: strings.Count(prefix, ".") + 1, + TTL: uint32(ep.RecordTTL), + } + services = append(services, service) } - service := Service{ - Host: ep.Targets[0], - Text: ep.Labels["originalText"], - Key: etcdKeyFor(prefix + "." + dnsName), - TargetStrip: strings.Count(prefix, ".") + 1, - TTL: uint32(ep.RecordTTL), - } - services = append(services, service) } index := 0 for _, ep := range group { - if ep.RecordType != "TXT" { + if ep.RecordType != endpoint.RecordTypeTXT { continue } if index >= len(services) { From 65e13af9b7025cea2fc131d87c0bd17595101ff0 Mon Sep 17 00:00:00 2001 From: Cesar Wong Date: Mon, 3 Dec 2018 18:16:57 -0500 Subject: [PATCH 22/44] Add zone tag filter for AWS --- main.go | 2 + pkg/apis/externaldns/types.go | 3 ++ pkg/apis/externaldns/types_test.go | 4 ++ provider/aws.go | 36 +++++++++++++++ provider/aws_test.go | 72 +++++++++++++++++++++++++++--- provider/zone_tag_filter.go | 57 +++++++++++++++++++++++ provider/zone_tag_filter_test.go | 62 +++++++++++++++++++++++++ 7 files changed, 229 insertions(+), 7 deletions(-) create mode 100644 provider/zone_tag_filter.go create mode 100644 provider/zone_tag_filter_test.go diff --git a/main.go b/main.go index b32b5fd45..56d4e5777 100644 --- a/main.go +++ b/main.go @@ -99,6 +99,7 @@ func main() { domainFilter := provider.NewDomainFilter(cfg.DomainFilter) zoneIDFilter := provider.NewZoneIDFilter(cfg.ZoneIDFilter) zoneTypeFilter := provider.NewZoneTypeFilter(cfg.AWSZoneType) + zoneTagFilter := provider.NewZoneTagFilter(cfg.AWSZoneTagFilter) var p provider.Provider switch cfg.Provider { @@ -110,6 +111,7 @@ func main() { DomainFilter: domainFilter, ZoneIDFilter: zoneIDFilter, ZoneTypeFilter: zoneTypeFilter, + ZoneTagFilter: zoneTagFilter, BatchChangeSize: cfg.AWSBatchChangeSize, BatchChangeInterval: cfg.AWSBatchChangeInterval, EvaluateTargetHealth: cfg.AWSEvaluateTargetHealth, diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index 3e67a8bf9..5e50ea374 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -56,6 +56,7 @@ type Config struct { AlibabaCloudConfigFile string AlibabaCloudZoneType string AWSZoneType string + AWSZoneTagFilter []string AWSAssumeRole string AWSBatchChangeSize int AWSBatchChangeInterval time.Duration @@ -127,6 +128,7 @@ var defaultConfig = &Config{ DomainFilter: []string{}, AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json", AWSZoneType: "", + AWSZoneTagFilter: []string{}, AWSAssumeRole: "", AWSBatchChangeSize: 4000, AWSBatchChangeInterval: time.Second, @@ -241,6 +243,7 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("alibaba-cloud-config-file", "When using the Alibaba Cloud provider, specify the Alibaba Cloud configuration file (required when --provider=alibabacloud").Default(defaultConfig.AlibabaCloudConfigFile).StringVar(&cfg.AlibabaCloudConfigFile) app.Flag("alibaba-cloud-zone-type", "When using the Alibaba Cloud provider, filter for zones of this type (optional, options: public, private)").Default(defaultConfig.AlibabaCloudZoneType).EnumVar(&cfg.AlibabaCloudZoneType, "", "public", "private") app.Flag("aws-zone-type", "When using the AWS provider, filter for zones of this type (optional, options: public, private)").Default(defaultConfig.AWSZoneType).EnumVar(&cfg.AWSZoneType, "", "public", "private") + app.Flag("aws-zone-tags", "When using the AWS provider, filter for zones with these tags").Default("").StringsVar(&cfg.AWSZoneTagFilter) app.Flag("aws-assume-role", "When using the AWS provider, assume this IAM role. Useful for hosted zones in another AWS account. Specify the full ARN, e.g. `arn:aws:iam::123455567:role/external-dns` (optional)").Default(defaultConfig.AWSAssumeRole).StringVar(&cfg.AWSAssumeRole) app.Flag("aws-batch-change-size", "When using the AWS provider, set the maximum number of changes that will be applied in each batch.").Default(strconv.Itoa(defaultConfig.AWSBatchChangeSize)).IntVar(&cfg.AWSBatchChangeSize) app.Flag("aws-batch-change-interval", "When using the AWS provider, set the interval between batch changes.").Default(defaultConfig.AWSBatchChangeInterval.String()).DurationVar(&cfg.AWSBatchChangeInterval) diff --git a/pkg/apis/externaldns/types_test.go b/pkg/apis/externaldns/types_test.go index 87740640f..bdee0a22d 100644 --- a/pkg/apis/externaldns/types_test.go +++ b/pkg/apis/externaldns/types_test.go @@ -43,6 +43,7 @@ var ( ZoneIDFilter: []string{""}, AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json", AWSZoneType: "", + AWSZoneTagFilter: []string{""}, AWSAssumeRole: "", AWSBatchChangeSize: 4000, AWSBatchChangeInterval: time.Second, @@ -94,6 +95,7 @@ var ( ZoneIDFilter: []string{"/hostedzone/ZTST1", "/hostedzone/ZTST2"}, AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json", AWSZoneType: "private", + AWSZoneTagFilter: []string{"tag=foo"}, AWSAssumeRole: "some-other-role", AWSBatchChangeSize: 100, AWSBatchChangeInterval: time.Second * 2, @@ -189,6 +191,7 @@ func TestParseFlags(t *testing.T) { "--zone-id-filter=/hostedzone/ZTST1", "--zone-id-filter=/hostedzone/ZTST2", "--aws-zone-type=private", + "--aws-zone-tags=tag=foo", "--aws-assume-role=some-other-role", "--aws-batch-change-size=100", "--aws-batch-change-interval=2s", @@ -248,6 +251,7 @@ func TestParseFlags(t *testing.T) { "EXTERNAL_DNS_TLS_CLIENT_CERT_KEY": "/path/to/key.pem", "EXTERNAL_DNS_ZONE_ID_FILTER": "/hostedzone/ZTST1\n/hostedzone/ZTST2", "EXTERNAL_DNS_AWS_ZONE_TYPE": "private", + "EXTERNAL_DNS_AWS_ZONE_TAGS": "tag=foo", "EXTERNAL_DNS_AWS_ASSUME_ROLE": "some-other-role", "EXTERNAL_DNS_AWS_BATCH_CHANGE_SIZE": "100", "EXTERNAL_DNS_AWS_BATCH_CHANGE_INTERVAL": "2s", diff --git a/provider/aws.go b/provider/aws.go index 7ec5e79aa..ec14c8d35 100644 --- a/provider/aws.go +++ b/provider/aws.go @@ -85,6 +85,7 @@ type Route53API interface { ChangeResourceRecordSets(*route53.ChangeResourceRecordSetsInput) (*route53.ChangeResourceRecordSetsOutput, error) CreateHostedZone(*route53.CreateHostedZoneInput) (*route53.CreateHostedZoneOutput, error) ListHostedZonesPages(input *route53.ListHostedZonesInput, fn func(resp *route53.ListHostedZonesOutput, lastPage bool) (shouldContinue bool)) error + ListTagsForResource(input *route53.ListTagsForResourceInput) (*route53.ListTagsForResourceOutput, error) } // AWSProvider is an implementation of Provider for AWS Route53. @@ -100,6 +101,8 @@ type AWSProvider struct { zoneIDFilter ZoneIDFilter // filter hosted zones by type (e.g. private or public) zoneTypeFilter ZoneTypeFilter + // filter hosted zones by tags + zoneTagFilter ZoneTagFilter } // AWSConfig contains configuration to create a new AWS provider. @@ -107,6 +110,7 @@ type AWSConfig struct { DomainFilter DomainFilter ZoneIDFilter ZoneIDFilter ZoneTypeFilter ZoneTypeFilter + ZoneTagFilter ZoneTagFilter BatchChangeSize int BatchChangeInterval time.Duration EvaluateTargetHealth bool @@ -145,6 +149,7 @@ func NewAWSProvider(awsConfig AWSConfig) (*AWSProvider, error) { domainFilter: awsConfig.DomainFilter, zoneIDFilter: awsConfig.ZoneIDFilter, zoneTypeFilter: awsConfig.ZoneTypeFilter, + zoneTagFilter: awsConfig.ZoneTagFilter, batchChangeSize: awsConfig.BatchChangeSize, batchChangeInterval: awsConfig.BatchChangeInterval, evaluateTargetHealth: awsConfig.EvaluateTargetHealth, @@ -158,6 +163,7 @@ func NewAWSProvider(awsConfig AWSConfig) (*AWSProvider, error) { func (p *AWSProvider) Zones() (map[string]*route53.HostedZone, error) { zones := make(map[string]*route53.HostedZone) + var tagErr error f := func(resp *route53.ListHostedZonesOutput, lastPage bool) (shouldContinue bool) { for _, zone := range resp.HostedZones { if !p.zoneIDFilter.Match(aws.StringValue(zone.Id)) { @@ -172,6 +178,18 @@ func (p *AWSProvider) Zones() (map[string]*route53.HostedZone, error) { continue } + // Only fetch tags if a tag filter was specified + if !p.zoneTagFilter.IsEmpty() { + tags, err := p.tagsForZone(*zone.Id) + if err != nil { + tagErr = err + return false + } + if !p.zoneTagFilter.Match(tags) { + continue + } + } + zones[aws.StringValue(zone.Id)] = zone } @@ -182,6 +200,9 @@ func (p *AWSProvider) Zones() (map[string]*route53.HostedZone, error) { if err != nil { return nil, err } + if tagErr != nil { + return nil, tagErr + } for _, zone := range zones { log.Debugf("Considering zone: %s (domain: %s)", aws.StringValue(zone.Id), aws.StringValue(zone.Name)) @@ -412,6 +433,21 @@ func (p *AWSProvider) newChange(action string, endpoint *endpoint.Endpoint) *rou return change } +func (p *AWSProvider) tagsForZone(zoneID string) (map[string]string, error) { + response, err := p.client.ListTagsForResource(&route53.ListTagsForResourceInput{ + ResourceType: aws.String("hostedzone"), + ResourceId: aws.String(zoneID), + }) + if err != nil { + return nil, err + } + tagMap := map[string]string{} + for _, tag := range response.ResourceTagSet.Tags { + tagMap[*tag.Key] = *tag.Value + } + return tagMap, nil +} + func batchChangeSet(cs []*route53.Change, batchSize int) [][]*route53.Change { if len(cs) <= batchSize { return [][]*route53.Change{cs} diff --git a/provider/aws_test.go b/provider/aws_test.go index 6852b6df2..8ffbc51ba 100644 --- a/provider/aws_test.go +++ b/provider/aws_test.go @@ -50,6 +50,7 @@ var _ Route53API = &Route53APIStub{} type Route53APIStub struct { zones map[string]*route53.HostedZone recordSets map[string]map[string][]*route53.ResourceRecordSet + zoneTags map[string][]*route53.Tag m dynamicMock } @@ -66,6 +67,7 @@ func NewRoute53APIStub() *Route53APIStub { return &Route53APIStub{ zones: make(map[string]*route53.HostedZone), recordSets: make(map[string]map[string][]*route53.ResourceRecordSet), + zoneTags: make(map[string][]*route53.Tag), } } @@ -95,6 +97,20 @@ func wildcardEscape(s string) string { return s } +func (r *Route53APIStub) ListTagsForResource(input *route53.ListTagsForResourceInput) (*route53.ListTagsForResourceOutput, error) { + if aws.StringValue(input.ResourceType) == "hostedzone" { + tags := r.zoneTags[aws.StringValue(input.ResourceId)] + return &route53.ListTagsForResourceOutput{ + ResourceTagSet: &route53.ResourceTagSet{ + ResourceId: input.ResourceId, + ResourceType: input.ResourceType, + Tags: tags, + }, + }, nil + } + return &route53.ListTagsForResourceOutput{}, nil +} + func (r *Route53APIStub) ChangeResourceRecordSets(input *route53.ChangeResourceRecordSetsInput) (*route53.ChangeResourceRecordSetsOutput, error) { if r.m.isMocked("ChangeResourceRecordSets", input) { return r.m.ChangeResourceRecordSets(input) @@ -231,15 +247,17 @@ func TestAWSZones(t *testing.T) { msg string zoneIDFilter ZoneIDFilter zoneTypeFilter ZoneTypeFilter + zoneTagFilter ZoneTagFilter expectedZones map[string]*route53.HostedZone }{ - {"no filter", NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), allZones}, - {"public filter", NewZoneIDFilter([]string{}), NewZoneTypeFilter("public"), publicZones}, - {"private filter", NewZoneIDFilter([]string{}), NewZoneTypeFilter("private"), privateZones}, - {"unknown filter", NewZoneIDFilter([]string{}), NewZoneTypeFilter("unknown"), noZones}, - {"zone id filter", NewZoneIDFilter([]string{"/hostedzone/zone-3.ext-dns-test-2.teapot.zalan.do."}), NewZoneTypeFilter(""), privateZones}, + {"no filter", NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), NewZoneTagFilter([]string{}), allZones}, + {"public filter", NewZoneIDFilter([]string{}), NewZoneTypeFilter("public"), NewZoneTagFilter([]string{}), publicZones}, + {"private filter", NewZoneIDFilter([]string{}), NewZoneTypeFilter("private"), NewZoneTagFilter([]string{}), privateZones}, + {"unknown filter", NewZoneIDFilter([]string{}), NewZoneTypeFilter("unknown"), NewZoneTagFilter([]string{}), noZones}, + {"zone id filter", NewZoneIDFilter([]string{"/hostedzone/zone-3.ext-dns-test-2.teapot.zalan.do."}), NewZoneTypeFilter(""), NewZoneTagFilter([]string{}), privateZones}, + {"tag filter", NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), NewZoneTagFilter([]string{"zone=3"}), privateZones}, } { - provider, _ := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), ti.zoneIDFilter, ti.zoneTypeFilter, defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{}) + provider, _ := newAWSProviderWithTagFilter(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), ti.zoneIDFilter, ti.zoneTypeFilter, ti.zoneTagFilter, defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{}) zones, err := provider.Zones() require.NoError(t, err) @@ -1027,8 +1045,11 @@ func escapeAWSRecords(t *testing.T, provider *AWSProvider, zone string) { require.NoError(t, err) } } - func newAWSProvider(t *testing.T, domainFilter DomainFilter, zoneIDFilter ZoneIDFilter, zoneTypeFilter ZoneTypeFilter, evaluateTargetHealth, dryRun bool, records []*endpoint.Endpoint) (*AWSProvider, *Route53APIStub) { + return newAWSProviderWithTagFilter(t, domainFilter, zoneIDFilter, zoneTypeFilter, NewZoneTagFilter([]string{}), evaluateTargetHealth, dryRun, records) +} + +func newAWSProviderWithTagFilter(t *testing.T, domainFilter DomainFilter, zoneIDFilter ZoneIDFilter, zoneTypeFilter ZoneTypeFilter, zoneTagFilter ZoneTagFilter, evaluateTargetHealth, dryRun bool, records []*endpoint.Endpoint) (*AWSProvider, *Route53APIStub) { client := NewRoute53APIStub() provider := &AWSProvider{ @@ -1039,6 +1060,7 @@ func newAWSProvider(t *testing.T, domainFilter DomainFilter, zoneIDFilter ZoneID domainFilter: domainFilter, zoneIDFilter: zoneIDFilter, zoneTypeFilter: zoneTypeFilter, + zoneTagFilter: zoneTagFilter, dryRun: false, } @@ -1067,6 +1089,8 @@ func newAWSProvider(t *testing.T, domainFilter DomainFilter, zoneIDFilter ZoneID Config: &route53.HostedZoneConfig{PrivateZone: aws.Bool(false)}, }) + setupZoneTags(provider.client.(*Route53APIStub)) + setupAWSRecords(t, provider, records) provider.dryRun = dryRun @@ -1074,6 +1098,40 @@ func newAWSProvider(t *testing.T, domainFilter DomainFilter, zoneIDFilter ZoneID return provider, client } +func setupZoneTags(client *Route53APIStub) { + addZoneTags(client.zoneTags, "/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do.", map[string]string{ + "zone-1-tag-1": "tag-1-value", + "domain": "test-2", + "zone": "1", + }) + addZoneTags(client.zoneTags, "/hostedzone/zone-2.ext-dns-test-2.teapot.zalan.do.", map[string]string{ + "zone-2-tag-1": "tag-1-value", + "domain": "test-2", + "zone": "2", + }) + addZoneTags(client.zoneTags, "/hostedzone/zone-3.ext-dns-test-2.teapot.zalan.do.", map[string]string{ + "zone-3-tag-1": "tag-1-value", + "domain": "test-2", + "zone": "3", + }) + addZoneTags(client.zoneTags, "/hostedzone/zone-4.ext-dns-test-2.teapot.zalan.do.", map[string]string{ + "zone-4-tag-1": "tag-1-value", + "domain": "test-3", + "zone": "4", + }) +} + +func addZoneTags(tagMap map[string][]*route53.Tag, zoneID string, tags map[string]string) { + tagList := make([]*route53.Tag, 0, len(tags)) + for k, v := range tags { + tagList = append(tagList, &route53.Tag{ + Key: aws.String(k), + Value: aws.String(v), + }) + } + tagMap[zoneID] = tagList +} + func validateRecords(t *testing.T, records []*route53.ResourceRecordSet, expected []*route53.ResourceRecordSet) { assert.Equal(t, expected, records) } diff --git a/provider/zone_tag_filter.go b/provider/zone_tag_filter.go new file mode 100644 index 000000000..c40ab06e9 --- /dev/null +++ b/provider/zone_tag_filter.go @@ -0,0 +1,57 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provider + +import ( + "strings" +) + +// ZoneTagFilter holds a list of zone tags to filter by +type ZoneTagFilter struct { + zoneTags []string +} + +// NewZoneTagFilter returns a new ZoneTagFilter given a list of zone tags +func NewZoneTagFilter(tags []string) ZoneTagFilter { + if len(tags) == 1 && len(tags[0]) == 0 { + tags = []string{} + } + return ZoneTagFilter{zoneTags: tags} +} + +// Match checks whether a zone's set of tags matches the provided tag values +func (f ZoneTagFilter) Match(tagsMap map[string]string) bool { + for _, tagFilter := range f.zoneTags { + filterParts := strings.SplitN(tagFilter, "=", 2) + switch len(filterParts) { + case 1: + if _, hasTag := tagsMap[filterParts[0]]; !hasTag { + return false + } + case 2: + if value, hasTag := tagsMap[filterParts[0]]; !hasTag || value != filterParts[1] { + return false + } + } + } + return true +} + +// IsEmpty returns true if there are no tags for the filter +func (f ZoneTagFilter) IsEmpty() bool { + return len(f.zoneTags) == 0 +} diff --git a/provider/zone_tag_filter_test.go b/provider/zone_tag_filter_test.go new file mode 100644 index 000000000..9574e68eb --- /dev/null +++ b/provider/zone_tag_filter_test.go @@ -0,0 +1,62 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provider + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestZoneTagFilterMatch(t *testing.T) { + for _, tc := range []struct { + name string + zoneTagFilter []string + zoneTags map[string]string + matches bool + }{ + { + "single tag no match", []string{"tag1=value1"}, map[string]string{"tag0": "value0"}, false, + }, + { + "single tag matches", []string{"tag1=value1"}, map[string]string{"tag1": "value1"}, true, + }, + { + "multiple tags no value match", []string{"tag1=value1"}, map[string]string{"tag0": "value0", "tag1": "value2"}, false, + }, + { + "multiple tags matches", []string{"tag1=value1"}, map[string]string{"tag0": "value0", "tag1": "value1"}, true, + }, + { + "tag name no match", []string{"tag1"}, map[string]string{"tag0": "value0"}, false, + }, + { + "tag name matches", []string{"tag1"}, map[string]string{"tag1": "value1"}, true, + }, + { + "multiple filter no match", []string{"tag1=value1", "tag2=value2"}, map[string]string{"tag1": "value1"}, false, + }, + { + "multiple filter matches", []string{"tag1=value1", "tag2=value2"}, map[string]string{"tag2": "value2", "tag1": "value1", "tag3": "value3"}, true, + }, + } { + zoneTagFilter := NewZoneTagFilter(tc.zoneTagFilter) + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.matches, zoneTagFilter.Match(tc.zoneTags)) + }) + } +} From 2a6d9ae8981f7a232c38f65d9c0b31abc20013d4 Mon Sep 17 00:00:00 2001 From: Sanyu Melwani <1154057+sanyu@users.noreply.github.com> Date: Mon, 10 Dec 2018 17:26:18 +1100 Subject: [PATCH 23/44] Removed extractTarget --- provider/dyn.go | 21 --------------------- provider/dyn_test.go | 16 ---------------- 2 files changed, 37 deletions(-) diff --git a/provider/dyn.go b/provider/dyn.go index fd0280250..e34999803 100644 --- a/provider/dyn.go +++ b/provider/dyn.go @@ -231,27 +231,6 @@ func merge(updateOld, updateNew []*endpoint.Endpoint) []*endpoint.Endpoint { return result } -// extractTarget populates the correct field given a record type. -// See dynect.DataBlock comments for details. Empty response means nothing -// was populated - basically an error -func extractTarget(recType string, data *dynect.DataBlock) string { - result := "" - if recType == endpoint.RecordTypeA { - result = data.Address - } - - if recType == endpoint.RecordTypeCNAME { - result = data.CName - result = strings.TrimSuffix(result, ".") - } - - if recType == endpoint.RecordTypeTXT { - result = data.TxtData - } - - return result -} - func apiRetryLoop(f func() error) error { var err error for i := 0; i < dynMaxRetriesOnErrRateLimited; i++ { diff --git a/provider/dyn_test.go b/provider/dyn_test.go index e93bbd5ce..f2c2fa6d6 100644 --- a/provider/dyn_test.go +++ b/provider/dyn_test.go @@ -168,22 +168,6 @@ func TestDynMerge_NoUpdateIfTTLUnchanged(t *testing.T) { assert.Equal(t, 0, len(merged)) } -func TestDyn_extractTarget(t *testing.T) { - tests := []struct { - recordType string - block *dynect.DataBlock - target string - }{ - {"A", &dynect.DataBlock{Address: "address"}, "address"}, - {"CNAME", &dynect.DataBlock{CName: "name."}, "name"}, // note trailing dot is trimmed for CNAMEs - {"TXT", &dynect.DataBlock{TxtData: "text."}, "text."}, - } - - for _, tc := range tests { - assert.Equal(t, tc.target, extractTarget(tc.recordType, tc.block)) - } -} - func TestDyn_endpointToRecord(t *testing.T) { tests := []struct { ep *endpoint.Endpoint From 7747db2351728a4c4fed6519f6f87071f8a61cd1 Mon Sep 17 00:00:00 2001 From: THEBAULT Julien Date: Wed, 5 Dec 2018 20:19:52 +0100 Subject: [PATCH 24/44] Update coredns tutorial with RBAC manifest (see #791) --- docs/tutorials/coredns.md | 79 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 75 insertions(+), 4 deletions(-) diff --git a/docs/tutorials/coredns.md b/docs/tutorials/coredns.md index 12a357dc0..a8554d456 100644 --- a/docs/tutorials/coredns.md +++ b/docs/tutorials/coredns.md @@ -86,8 +86,10 @@ helm install --name my-coredns --values values.yaml stable/coredns ## Installing ExternalDNS ### Install external ExternalDNS ETCD_URLS is configured to etcd client service address. -``` -$ cat external-dns.yaml + +#### Manifest (for clusters without RBAC enabled) + +```yaml apiVersion: apps/v1 kind: Deployment metadata: @@ -97,7 +99,7 @@ spec: strategy: type: Recreate selector: - matchLabels: + matchLabels: app: external-dns template: metadata: @@ -114,7 +116,76 @@ spec: env: - name: ETCD_URLS value: http://10.105.68.165:2379 -$ kubectl apply -f external-dns.yaml +``` + +#### Manifest (for clusters with RBAC enabled) + +```yaml +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: ClusterRole +metadata: + name: external-dns +rules: +- apiGroups: [""] + resources: ["services"] + verbs: ["get","watch","list"] +- apiGroups: [""] + resources: ["pods"] + verbs: ["get","watch","list"] +- apiGroups: ["extensions"] + resources: ["ingresses"] + verbs: ["get","watch","list"] +- apiGroups: [""] + resources: ["nodes"] + verbs: ["list"] +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: ClusterRoleBinding +metadata: + name: external-dns-viewer +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: external-dns +subjects: +- kind: ServiceAccount + name: external-dns + namespace: kube-system +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: external-dns + namespace: kube-system +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: external-dns + namespace: kube-system +spec: + strategy: + type: Recreate + selector: + matchLabels: + app: external-dns + template: + metadata: + labels: + app: external-dns + spec: + serviceAccountName: external-dns + containers: + - name: external-dns + image: registry.opensource.zalan.do/teapot/external-dns:latest + args: + - --source=ingress + - --provider=coredns + - --log-level=debug # debug only + env: + - name: ETCD_URLS + value: http://10.105.68.165:2379 ``` ## Enable the ingress controller From fc245c088d759caa22bc3c6014eced928d436b3c Mon Sep 17 00:00:00 2001 From: xunpan Date: Fri, 7 Dec 2018 21:52:06 -0500 Subject: [PATCH 25/44] avoid unnecessary updating for CRD resource with test updated --- source/crd.go | 5 +++++ source/crd_test.go | 46 ++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/source/crd.go b/source/crd.go index 79b308ad4..4c5a2a698 100644 --- a/source/crd.go +++ b/source/crd.go @@ -118,6 +118,11 @@ func (cs *crdSource) Endpoints() ([]*endpoint.Endpoint, error) { for _, dnsEndpoint := range result.Items { endpoints = append(endpoints, dnsEndpoint.Spec.Endpoints...) + + if dnsEndpoint.Status.ObservedGeneration == dnsEndpoint.Generation { + continue + } + dnsEndpoint.Status.ObservedGeneration = dnsEndpoint.Generation // Update the ObservedGeneration _, err = cs.UpdateStatus(&dnsEndpoint) diff --git a/source/crd_test.go b/source/crd_test.go index a069f2114..928a35685 100644 --- a/source/crd_test.go +++ b/source/crd_test.go @@ -18,6 +18,7 @@ package source import ( "bytes" + "encoding/json" "fmt" "io" "io/ioutil" @@ -54,20 +55,21 @@ func objBody(codec runtime.Codec, obj runtime.Object) io.ReadCloser { return ioutil.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(codec, obj)))) } -func startCRDServerToServeTargets(endpoints []*endpoint.Endpoint, apiVersion, kind, namespace, name string) rest.Interface { +func startCRDServerToServeTargets(endpoints []*endpoint.Endpoint, apiVersion, kind, namespace, name string, t *testing.T) rest.Interface { groupVersion, _ := schema.ParseGroupVersion(apiVersion) scheme := runtime.NewScheme() addKnownTypes(scheme, groupVersion) dnsEndpointList := endpoint.DNSEndpointList{} - dnsEndpoint := endpoint.DNSEndpoint{ + dnsEndpoint := &endpoint.DNSEndpoint{ TypeMeta: metav1.TypeMeta{ APIVersion: apiVersion, Kind: kind, }, ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, + Name: name, + Namespace: namespace, + Generation: 1, }, Spec: endpoint.DNSEndpointSpec{ Endpoints: endpoints, @@ -88,10 +90,18 @@ func startCRDServerToServeTargets(endpoints []*endpoint.Endpoint, apiVersion, ki case p == "/apis/"+apiVersion+"/"+strings.ToLower(kind)+"s" && m == http.MethodGet: fallthrough case p == "/apis/"+apiVersion+"/namespaces/"+namespace+"/"+strings.ToLower(kind)+"s" && m == http.MethodGet: - dnsEndpointList.Items = append(dnsEndpointList.Items, dnsEndpoint) + dnsEndpointList.Items = dnsEndpointList.Items[:0] + dnsEndpointList.Items = append(dnsEndpointList.Items, *dnsEndpoint) return &http.Response{StatusCode: http.StatusOK, Header: defaultHeader(), Body: objBody(codec, &dnsEndpointList)}, nil case strings.HasPrefix(p, "/apis/"+apiVersion+"/namespaces/") && strings.HasSuffix(p, strings.ToLower(kind)+"s") && m == http.MethodGet: return &http.Response{StatusCode: http.StatusOK, Header: defaultHeader(), Body: objBody(codec, &dnsEndpointList)}, nil + case p == "/apis/"+apiVersion+"/namespaces/"+namespace+"/"+strings.ToLower(kind)+"s/"+name+"/status" && m == http.MethodPut: + decoder := json.NewDecoder(req.Body) + + var body endpoint.DNSEndpoint + decoder.Decode(&body) + dnsEndpoint.Status.ObservedGeneration = body.Status.ObservedGeneration + return &http.Response{StatusCode: http.StatusOK, Header: defaultHeader(), Body: objBody(codec, dnsEndpoint)}, nil default: return nil, fmt.Errorf("unexpected request: %#v\n%#v", req.URL, req) } @@ -200,6 +210,8 @@ func testCRDSourceEndpoints(t *testing.T) { apiVersion: "test.k8s.io/v1alpha1", registeredKind: "DNSEndpoint", kind: "DNSEndpoint", + namespace: "foo", + registeredNamespace: "foo", endpoints: []*endpoint.Endpoint{ {DNSName: "abc.example.org", Targets: endpoint.Targets{"1.2.3.4"}, @@ -216,6 +228,8 @@ func testCRDSourceEndpoints(t *testing.T) { apiVersion: "test.k8s.io/v1alpha1", registeredKind: "DNSEndpoint", kind: "DNSEndpoint", + namespace: "foo", + registeredNamespace: "foo", endpoints: []*endpoint.Endpoint{ {DNSName: "abc.example.org", Targets: endpoint.Targets{"1.2.3.4"}, @@ -233,7 +247,7 @@ func testCRDSourceEndpoints(t *testing.T) { }, } { t.Run(ti.title, func(t *testing.T) { - restClient := startCRDServerToServeTargets(ti.endpoints, ti.registeredAPIVersion, ti.registeredKind, ti.registeredNamespace, "") + restClient := startCRDServerToServeTargets(ti.endpoints, ti.registeredAPIVersion, ti.registeredKind, ti.registeredNamespace, "test", t) groupVersion, err := schema.ParseGroupVersion(ti.apiVersion) require.NoError(t, err) @@ -253,8 +267,28 @@ func testCRDSourceEndpoints(t *testing.T) { return } + if err == nil { + validateCRDResource(t, cs, ti.expectError) + } + // Validate received endpoints against expected endpoints. validateEndpoints(t, receivedEndpoints, ti.endpoints) }) } } + +func validateCRDResource(t *testing.T, src Source, expectError bool) { + cs := src.(*crdSource) + result, err := cs.List(&metav1.ListOptions{}) + if expectError { + require.Errorf(t, err, "Received err %v", err) + } else { + require.NoErrorf(t, err, "Received err %v", err) + } + + for _, dnsEndpoint := range result.Items { + if dnsEndpoint.Status.ObservedGeneration != dnsEndpoint.Generation { + require.Errorf(t, err, "Unexpected CRD resource result: ObservedGenerations <%v> is not equal to Generation<%v>", dnsEndpoint.Status.ObservedGeneration, dnsEndpoint.Generation) + } + } +} From 374bb9235a968b4f2def87178a0facfde5cc09de Mon Sep 17 00:00:00 2001 From: Adrian Rangel Date: Wed, 19 Dec 2018 02:08:20 -0600 Subject: [PATCH 26/44] fix commands to cleanup --- docs/tutorials/cloudflare.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/cloudflare.md b/docs/tutorials/cloudflare.md index 308cb8186..72c39260a 100644 --- a/docs/tutorials/cloudflare.md +++ b/docs/tutorials/cloudflare.md @@ -193,6 +193,6 @@ This should show the external IP address of the service as the A record for your Now that we have verified that ExternalDNS will automatically manage Cloudflare DNS records, we can delete the tutorial's example: ``` -$ kubectl delete service -f nginx.yaml -$ kubectl delete service -f externaldns.yaml +$ kubectl delete -f nginx.yaml +$ kubectl delete -f externaldns.yaml ``` From cea58909f0c118f19754c9bd059b932bc19c5f75 Mon Sep 17 00:00:00 2001 From: Wade Lee Date: Thu, 20 Dec 2018 16:31:55 +0800 Subject: [PATCH 27/44] Update coredns.md Make the DNS service IP consistent with `my-coredns-coredns` in example --- docs/tutorials/coredns.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/coredns.md b/docs/tutorials/coredns.md index a8554d456..e20699c89 100644 --- a/docs/tutorials/coredns.md +++ b/docs/tutorials/coredns.md @@ -228,8 +228,8 @@ nginx nginx.example.org 10.0.2.15 80 2m $ 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.102.213.122 nginx.example.org +short -dnstools# dig @10.102.213.122 nginx.example.org +short +dnstools# dig @10.100.4.143 nginx.example.org +short +dnstools# dig @10.100.4.143 nginx.example.org +short 10.0.2.15 dnstools# ``` From a19cc0a3d0b74a2d0975e3ae886a56eb25723f9e Mon Sep 17 00:00:00 2001 From: Zach Yam Date: Thu, 20 Dec 2018 23:30:52 +0100 Subject: [PATCH 28/44] Add metrics info to FAQ --- docs/faq.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/faq.md b/docs/faq.md index d5a1666ab..99a622ea7 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -28,7 +28,7 @@ ExternalDNS can solve this for you as well. ### Which DNS providers are supported? -Currently, the following providers are supported: +Currently, the following providers are supported: - Google CloudDNS - AWS Route 53 @@ -155,6 +155,18 @@ CNAMEs cannot co-exist with other records, therefore you can use the `--txt-pref You need to add either https://www.googleapis.com/auth/ndev.clouddns.readwrite or https://www.googleapis.com/auth/cloud-platform on your instance group's scope. +### What metrics can I get from ExternalDNS and what do they mean? + +ExternalDNS exposes 2 types of metrics: Sources and Registry errors. + +`Source`s are mostly Kubernetes API objects. Examples of `source` errors may be connection errors to the Kubernetes API server itself or missing RBAC permissions. It can also stem from incompatible configuration in the objects itself like invalid characters, processing a broken fqdnTemplate etc. + +`Registry` errors are mostly Provider errors, unless there's some coding flaw in the registry package. Provider errors often arise due to accessing their APIs due to network or missing cloud-provider permissions when reading records. When applying a changeset, errors arise when the changeset applied is incompatible with the current state, in which case ExternalDNS can get stuck forever. + +In case of an increased error count, you could correlate them with the `http_request_duration_seconds{handler="instrumented_http"}` metric which should show increased numbers for status codes 4xx (permissions, configuration, invalid changeset) or 5xx (apiserver down). + +You can use the host label in the metric to figure out if the request was against the Kubernetes API server (Source errors) or the DNS provider API (Registry/Provider errors). + ### How can I run ExternalDNS under a specific GCP Service Account, e.g. to access DNS records in other projects? Have a look at https://github.com/linki/mate/blob/v0.6.2/examples/google/README.md#permissions From 5aee5ad345beba79ab138954281b288daf701ea3 Mon Sep 17 00:00:00 2001 From: Pascal Kutscha Date: Sat, 22 Dec 2018 21:27:48 +0100 Subject: [PATCH 29/44] Update cloudflare.md --- docs/tutorials/cloudflare.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/cloudflare.md b/docs/tutorials/cloudflare.md index 308cb8186..c84e4f432 100644 --- a/docs/tutorials/cloudflare.md +++ b/docs/tutorials/cloudflare.md @@ -16,7 +16,7 @@ Snippet from [Cloudflare - Getting Started](https://api.cloudflare.com/#getting- >Cloudflare's API exposes the entire Cloudflare infrastructure via a standardized programmatic interface. Using Cloudflare's API, you can do just about anything you can do on cloudflare.com via the customer dashboard. ->The Cloudflare API is a RESTful API based on HTTPS requests and JSON responses. If you are registered with Cloudflare, you can obtain your API key from the bottom of the "My Account" page, found here: [Go to My account](https://www.cloudflare.com/a/account). +>The Cloudflare API is a RESTful API based on HTTPS requests and JSON responses. If you are registered with Cloudflare, you can obtain your API key from the bottom of the "My Account" page, found here: [Go to My account](https://dash.cloudflare.com/profile). The environment vars `CF_API_KEY` and `CF_API_EMAIL` will be needed to run ExternalDNS with Cloudflare. From d0de07c084d5021ee15adbc88116fa8776e10cad Mon Sep 17 00:00:00 2001 From: Denis Biondic Date: Mon, 24 Dec 2018 16:44:06 +0100 Subject: [PATCH 30/44] docs(azure): better security granuality concerning external dns service principal --- docs/tutorials/azure.md | 59 +++++++++++++++++++++++++++++++---------- 1 file changed, 45 insertions(+), 14 deletions(-) diff --git a/docs/tutorials/azure.md b/docs/tutorials/azure.md index cdd45e7f6..51f6d77fc 100644 --- a/docs/tutorials/azure.md +++ b/docs/tutorials/azure.md @@ -61,13 +61,18 @@ The `resourceGroup` is the Resource Group created in a previous step. The `aadClientID` and `aaClientSecret` are assoiated with the Service Principal, that you need to create next. ### Creating service principal -A Service Principal with a minimum access level of contribute to the resource group containing the Azure DNS zone(s) is necessary for ExternalDNS to be able to edit DNS records. This is an Azure CLI example on how to query the Azure API for the information required for the Resource Group and DNS zone you would have already created in previous steps. +A Service Principal with a minimum access level of `contributor` to the DNS zone(s) and `reader` to the resource group containing the Azure DNS zone(s) is necessary for ExternalDNS to be able to edit DNS records. However, other more permissive access levels will work too (e.g. `contributor` to the resource group or the whole subscription). +This is an Azure CLI example on how to query the Azure API for the information required for the Resource Group and DNS zone you would have already created in previous steps. + +``` bash +> az login ``` ->az login -... -# find the relevant subscription and set the az context. id = subscriptionId value in the azure.json. ->az account list + +Find the relevant subscription and make sure it is selected (the same subscriptionId should be set into azure.json) + +``` bash +> az account list { "cloudName": "AzureCloud", "id": "", @@ -79,16 +84,15 @@ A Service Principal with a minimum access level of contribute to the resource gr "name": "name", "type": "user" } ->az account set -s id -... ->az group show --name externaldns -{ - "id": "/subscriptions/id/resourceGroups/externaldns", - ... -} -# use the id from the previous step in the scopes argument ->az ad sp create-for-rbac --role="Contributor" --scopes="/subscriptions/id/resourceGroups/externaldns" -n ExternalDnsServicePrincipal +# select the subscription +> az account set -s +... +``` +Create the service principal + +``` bash +> az ad sp create-for-rbac -n ExternalDnsServicePrincipal { "appId": "appId GUID", <-- aadClientId value ... @@ -97,6 +101,33 @@ A Service Principal with a minimum access level of contribute to the resource gr } ``` +Assign the rights for the service principal + +``` +# find out the resource ids of the resource group where the dns zone is deployed, and the dns zone itself +> az group show --name externaldns +{ + "id": "/subscriptions/id/resourceGroups/externaldns", + ... +} + +> az network dns zone show --name example.com -g externaldns +{ + "id": "/subscriptions/.../resourceGroups/externaldns/providers/Microsoft.Network/dnszones/example.com", + ... +} +``` +``` +# assign the rights to the created service principal, using the resource ids from previous step + +# 1. as a reader to the resource group +> az role assignment create --role "Reader" --assignee --scope + +# 2. as a contributor to DNS Zone itself +> az role assignment create --role "Contributor" --assignee --scope + +``` + Now you can create a file named 'azure.json' with values gathered above and with the structure of the example above. Use this file to create a Kubernetes secret: ``` From 3c646a39a1c6ee657c4271be9d9c718d750b19b9 Mon Sep 17 00:00:00 2001 From: Justin SB Date: Thu, 27 Dec 2018 12:34:57 -0500 Subject: [PATCH 31/44] Implement Stringer for planTableRow Makes for clearer log messages. --- plan/plan.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plan/plan.go b/plan/plan.go index 21bf5b677..cdbdf1837 100644 --- a/plan/plan.go +++ b/plan/plan.go @@ -17,6 +17,7 @@ limitations under the License. package plan import ( + "fmt" "strings" "github.com/kubernetes-incubator/external-dns/endpoint" @@ -78,6 +79,10 @@ type planTableRow struct { candidates []*endpoint.Endpoint } +func (t planTableRow) String() string { + return fmt.Sprintf("planTableRow{current=%v, candidates=%v}", t.current, t.candidates) +} + func (t planTable) addCurrent(e *endpoint.Endpoint) { dnsName := sanitizeDNSName(e.DNSName) if _, ok := t.rows[dnsName]; !ok { From 173ef2ea4b2f169f6d88985f5377923e55fba0d2 Mon Sep 17 00:00:00 2001 From: Justin SB Date: Thu, 27 Dec 2018 12:40:14 -0500 Subject: [PATCH 32/44] Normalize DNS names during planning Ensure that we don't consider names with and without a trailing dot differently at this stage. --- plan/plan.go | 16 ++++++++++------ plan/plan_test.go | 30 +++++++++++++++++------------- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/plan/plan.go b/plan/plan.go index 21bf5b677..f3cdc6c19 100644 --- a/plan/plan.go +++ b/plan/plan.go @@ -79,7 +79,7 @@ type planTableRow struct { } func (t planTable) addCurrent(e *endpoint.Endpoint) { - dnsName := sanitizeDNSName(e.DNSName) + dnsName := normalizeDNSName(e.DNSName) if _, ok := t.rows[dnsName]; !ok { t.rows[dnsName] = &planTableRow{} } @@ -87,7 +87,7 @@ func (t planTable) addCurrent(e *endpoint.Endpoint) { } func (t planTable) addCandidate(e *endpoint.Endpoint) { - dnsName := sanitizeDNSName(e.DNSName) + dnsName := normalizeDNSName(e.DNSName) if _, ok := t.rows[dnsName]; !ok { t.rows[dnsName] = &planTableRow{} } @@ -204,8 +204,12 @@ func filterRecordsForPlan(records []*endpoint.Endpoint) []*endpoint.Endpoint { return filtered } -// sanitizeDNSName checks if the DNS name is correct -// for now it only removes space and lower case -func sanitizeDNSName(dnsName string) string { - return strings.TrimSpace(strings.ToLower(dnsName)) +// normalizeDNSName converts a DNS name to a canonical form, so that we can use string equality +// it: removes space, converts to lower case, ensures there is a trailing dot +func normalizeDNSName(dnsName string) string { + s := strings.TrimSpace(strings.ToLower(dnsName)) + if !strings.HasSuffix(s, ".") { + s += "." + } + return s } diff --git a/plan/plan_test.go b/plan/plan_test.go index e36b343a6..daeb8caa4 100644 --- a/plan/plan_test.go +++ b/plan/plan_test.go @@ -385,54 +385,58 @@ func validateEntries(t *testing.T, entries, expected []*endpoint.Endpoint) { } } -func TestSanitizeDNSName(t *testing.T) { +func TestNormalizeDNSName(t *testing.T) { records := []struct { dnsName string expect string }{ { "3AAAA.FOO.BAR.COM ", - "3aaaa.foo.bar.com", + "3aaaa.foo.bar.com.", }, { - " example.foo.com", - "example.foo.com", + " example.foo.com.", + "example.foo.com.", }, { "example123.foo.com ", - "example123.foo.com", + "example123.foo.com.", }, { "foo", - "foo", + "foo.", }, { "123foo.bar", - "123foo.bar", + "123foo.bar.", }, { "foo.com", - "foo.com", + "foo.com.", + }, + { + "foo.com.", + "foo.com.", }, { "foo123.COM", - "foo123.com", + "foo123.com.", }, { "my-exaMple3.FOO.BAR.COM", - "my-example3.foo.bar.com", + "my-example3.foo.bar.com.", }, { " my-example1214.FOO-1235.BAR-foo.COM ", - "my-example1214.foo-1235.bar-foo.com", + "my-example1214.foo-1235.bar-foo.com.", }, { "my-example-my-example-1214.FOO-1235.BAR-foo.COM", - "my-example-my-example-1214.foo-1235.bar-foo.com", + "my-example-my-example-1214.foo-1235.bar-foo.com.", }, } for _, r := range records { - gotName := sanitizeDNSName(r.dnsName) + gotName := normalizeDNSName(r.dnsName) assert.Equal(t, r.expect, gotName) } } From bb6c5f18057cde75de1ad52d5c96fd75d61e38f0 Mon Sep 17 00:00:00 2001 From: Ivan Filippov Date: Thu, 3 Jan 2019 05:01:50 -0700 Subject: [PATCH 33/44] RFC2136 seems to require one IP Target per RRSET instead of multiple IPs per RRSET. --- provider/rfc2136.go | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/provider/rfc2136.go b/provider/rfc2136.go index 7f9856bad..153f5e8e0 100644 --- a/provider/rfc2136.go +++ b/provider/rfc2136.go @@ -240,25 +240,26 @@ func (r rfc2136Provider) UpdateRecord(ep *endpoint.Endpoint) error { func (r rfc2136Provider) AddRecord(ep *endpoint.Endpoint) error { log.Debugf("AddRecord.ep=%s", ep) + for _, target := range ep.Targets { + newRR := fmt.Sprintf("%s %d %s %s", ep.DNSName, ep.RecordTTL, ep.RecordType, target) + log.Debugf("Adding RR: %s", newRR) - newRR := fmt.Sprintf("%s %d %s %s", ep.DNSName, ep.RecordTTL, ep.RecordType, ep.Targets) - log.Debugf("Adding RR: %s", newRR) + rr, err := dns.NewRR(newRR) + if err != nil { + return fmt.Errorf("failed to build RR: %v", err) + } - rr, err := dns.NewRR(newRR) - if err != nil { - return fmt.Errorf("failed to build RR: %v", err) - } + rrs := make([]dns.RR, 1) + rrs[0] = rr - rrs := make([]dns.RR, 1) - rrs[0] = rr + m := new(dns.Msg) + m.SetUpdate(r.zoneName) + m.Insert(rrs) - m := new(dns.Msg) - m.SetUpdate(r.zoneName) - m.Insert(rrs) - - err = r.actions.SendMessage(m) - if err != nil { - return fmt.Errorf("RFC2136 query failed: %v", err) + err = r.actions.SendMessage(m) + if err != nil { + return fmt.Errorf("RFC2136 query failed: %v", err) + } } return nil From d93f6ec24e7701668281ff877a3a0778b7fc0402 Mon Sep 17 00:00:00 2001 From: Lachlan Cooper Date: Fri, 4 Jan 2019 10:49:20 +1100 Subject: [PATCH 34/44] Fix typos in rfc2136 provider The rfc2136Actions interface was misspelled. Signed-off-by: Lachlan Cooper --- provider/rfc2136.go | 6 +++--- provider/rfc2136_test.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/provider/rfc2136.go b/provider/rfc2136.go index 7f9856bad..572f2e3c0 100644 --- a/provider/rfc2136.go +++ b/provider/rfc2136.go @@ -43,7 +43,7 @@ type rfc2136Provider struct { // only consider hosted zones managing domains ending in this suffix domainFilter DomainFilter dryRun bool - actions rfc1236Actions + actions rfc2136Actions } var ( @@ -56,13 +56,13 @@ var ( } ) -type rfc1236Actions interface { +type rfc2136Actions interface { SendMessage(msg *dns.Msg) error IncomeTransfer(m *dns.Msg, a string) (env chan *dns.Envelope, err error) } // 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 DomainFilter, dryRun bool, actions rfc1236Actions) (Provider, error) { +func NewRfc2136Provider(host string, port int, zoneName string, insecure bool, keyName string, secret string, secretAlg string, axfr bool, domainFilter DomainFilter, dryRun bool, actions rfc2136Actions) (Provider, error) { secretAlgChecked, ok := tsigAlgs[secretAlg] if !ok { return nil, errors.Errorf("%s is not supported TSIG algorithm", secretAlg) diff --git a/provider/rfc2136_test.go b/provider/rfc2136_test.go index 63d10abb2..cb9ef8a2b 100644 --- a/provider/rfc2136_test.go +++ b/provider/rfc2136_test.go @@ -93,7 +93,7 @@ func createRfc2136StubProvider(stub *rfc2136Stub) (Provider, error) { return NewRfc2136Provider("", 0, "", false, "key", "secret", "hmac-sha512", true, DomainFilter{}, false, stub) } -func TestRfc1236GetRecords(t *testing.T) { +func TestRfc2136GetRecords(t *testing.T) { stub := newStub() err := stub.setOutput([]string{ "v4.barfoo.com 3600 TXT test1", From 736a01633e0212c234a35d765dac1ba3eb153722 Mon Sep 17 00:00:00 2001 From: Lachlan Cooper Date: Fri, 4 Jan 2019 11:02:29 +1100 Subject: [PATCH 35/44] Fix dry-run mode in rfc2136 provider In dry-run mode we need to return early to avoid sending messages. Fixes #816. Signed-off-by: Lachlan Cooper --- provider/rfc2136.go | 1 + 1 file changed, 1 insertion(+) diff --git a/provider/rfc2136.go b/provider/rfc2136.go index 7f9856bad..469566d34 100644 --- a/provider/rfc2136.go +++ b/provider/rfc2136.go @@ -293,6 +293,7 @@ func (r rfc2136Provider) RemoveRecord(ep *endpoint.Endpoint) error { func (r rfc2136Provider) SendMessage(msg *dns.Msg) error { if r.dryRun { log.Debugf("SendMessage.skipped") + return nil } log.Debugf("SendMessage") From 391b536c13627b9c4bad19d14087c9f045e4bc3c Mon Sep 17 00:00:00 2001 From: Adam Medzinski Date: Fri, 4 Jan 2019 14:55:48 +0100 Subject: [PATCH 36/44] Change default AWSBatchChangeSize to 1000 AWS API ChangeResourceRecordSets method only allows 1000 ResourceRecord elements in one call, so the previous value was not very useful. --- pkg/apis/externaldns/types.go | 2 +- pkg/apis/externaldns/types_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index 5e50ea374..5de4aca5c 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -130,7 +130,7 @@ var defaultConfig = &Config{ AWSZoneType: "", AWSZoneTagFilter: []string{}, AWSAssumeRole: "", - AWSBatchChangeSize: 4000, + AWSBatchChangeSize: 1000, AWSBatchChangeInterval: time.Second, AWSEvaluateTargetHealth: true, AzureConfigFile: "/etc/kubernetes/azure.json", diff --git a/pkg/apis/externaldns/types_test.go b/pkg/apis/externaldns/types_test.go index bdee0a22d..8a1f1854f 100644 --- a/pkg/apis/externaldns/types_test.go +++ b/pkg/apis/externaldns/types_test.go @@ -45,7 +45,7 @@ var ( AWSZoneType: "", AWSZoneTagFilter: []string{""}, AWSAssumeRole: "", - AWSBatchChangeSize: 4000, + AWSBatchChangeSize: 1000, AWSBatchChangeInterval: time.Second, AWSEvaluateTargetHealth: true, AzureConfigFile: "/etc/kubernetes/azure.json", From 9cc0fbf3e1ff57a161f089230ff972ce543aa524 Mon Sep 17 00:00:00 2001 From: Zach Seils Date: Fri, 4 Jan 2019 21:37:00 +0000 Subject: [PATCH 37/44] Correct Google Cloud DNS (ref: https://cloud.google.com/dns/) naming in docs --- README.md | 4 ++-- docs/contributing/getting-started.md | 2 +- docs/contributing/sources-and-providers.md | 4 ++-- docs/faq.md | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 25c2b5536..d2058e41b 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ ExternalDNS synchronizes exposed Kubernetes Services and Ingresses with DNS prov ## What It Does -Inspired by [Kubernetes DNS](https://github.com/kubernetes/dns), Kubernetes' cluster-internal DNS server, ExternalDNS makes Kubernetes resources discoverable via public DNS servers. Like KubeDNS, it retrieves a list of resources (Services, Ingresses, etc.) from the [Kubernetes API](https://kubernetes.io/docs/api/) to determine a desired list of DNS records. *Unlike* KubeDNS, however, it's not a DNS server itself, but merely configures other DNS providers accordingly—e.g. [AWS Route 53](https://aws.amazon.com/route53/) or [Google CloudDNS](https://cloud.google.com/dns/docs/). +Inspired by [Kubernetes DNS](https://github.com/kubernetes/dns), Kubernetes' cluster-internal DNS server, ExternalDNS makes Kubernetes resources discoverable via public DNS servers. Like KubeDNS, it retrieves a list of resources (Services, Ingresses, etc.) from the [Kubernetes API](https://kubernetes.io/docs/api/) to determine a desired list of DNS records. *Unlike* KubeDNS, however, it's not a DNS server itself, but merely configures other DNS providers accordingly—e.g. [AWS Route 53](https://aws.amazon.com/route53/) or [Google Cloud DNS](https://cloud.google.com/dns/docs/). In a broader sense, ExternalDNS allows you to control DNS records dynamically via Kubernetes resources in a DNS provider-agnostic way. @@ -24,7 +24,7 @@ To see ExternalDNS in action, have a look at this [video](https://www.youtube.co ## The Latest Release: v0.5 ExternalDNS' current release is `v0.5`. This version allows you to keep selected zones (via `--domain-filter`) synchronized with Ingresses and Services of `type=LoadBalancer` in various cloud providers: -* [Google CloudDNS](https://cloud.google.com/dns/docs/) +* [Google Cloud DNS](https://cloud.google.com/dns/docs/) * [AWS Route 53](https://aws.amazon.com/route53/) * [AWS Service Discovery](https://docs.aws.amazon.com/Route53/latest/APIReference/overview-service-discovery.html) * [AzureDNS](https://azure.microsoft.com/en-us/services/dns) diff --git a/docs/contributing/getting-started.md b/docs/contributing/getting-started.md index 8ce2325c6..9fe764c37 100644 --- a/docs/contributing/getting-started.md +++ b/docs/contributing/getting-started.md @@ -14,7 +14,7 @@ This list of endpoints is passed to the [Plan](../../plan) which determines the Once the difference has been figured out the list of intended changes is passed to a `Registry` which live in the [registry](../../registry) package. The registry is a wrapper and access point to DNS provider. Registry implements the ownership concept by marking owned records and filtering out records not owned by ExternalDNS before passing them to DNS provider. -The [provider](../../provider) is the adapter to the DNS provider, e.g. Google CloudDNS. It implements two methods: `ApplyChanges` to apply a set of changes filtered by `Registry` and `Records` to retrieve the current list of records from the DNS provider. +The [provider](../../provider) is the adapter to the DNS provider, e.g. Google Cloud DNS. It implements two methods: `ApplyChanges` to apply a set of changes filtered by `Registry` and `Records` to retrieve the current list of records from the DNS provider. The orchestration between the different components is controlled by the [controller](../../controller). diff --git a/docs/contributing/sources-and-providers.md b/docs/contributing/sources-and-providers.md index e5d1cd995..3d51b3598 100644 --- a/docs/contributing/sources-and-providers.md +++ b/docs/contributing/sources-and-providers.md @@ -29,7 +29,7 @@ All sources live in package `source`. ### Providers Providers are an abstraction over any kind of sink for desired Endpoints, e.g.: -* storing them in Google CloudDNS +* storing them in Google Cloud DNS * printing them to stdout for testing purposes * fanning out to multiple nested providers @@ -46,7 +46,7 @@ The interface tries to be generic and assumes a flat list of records for both fu All providers live in package `provider`. -* `GoogleProvider`: returns and creates DNS records in Google CloudDNS +* `GoogleProvider`: returns and creates DNS records in Google Cloud DNS * `AWSProvider`: returns and creates DNS records in AWS Route 53 * `AzureProvider`: returns and creates DNS records in Azure DNS * `InMemoryProvider`: Keeps a list of records in local memory diff --git a/docs/faq.md b/docs/faq.md index 927a5b9a4..38b72aac3 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -30,7 +30,7 @@ ExternalDNS can solve this for you as well. Currently, the following providers are supported: -- Google CloudDNS +- Google Cloud DNS - AWS Route 53 - AzureDNS - CloudFlare From 123cc398f1985544148819d7a75970b56c0d1535 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nick=20J=C3=BCttner?= Date: Mon, 7 Jan 2019 14:13:26 +0100 Subject: [PATCH 38/44] add security file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Nick Jüttner --- SECURITY_CONTACTS | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 SECURITY_CONTACTS diff --git a/SECURITY_CONTACTS b/SECURITY_CONTACTS new file mode 100644 index 000000000..16ccfb1fe --- /dev/null +++ b/SECURITY_CONTACTS @@ -0,0 +1,16 @@ +# Defined below are the security contacts for this repo. +# +# They are the contact point for the Product Security Team to reach out +# to for triaging and handling of incoming issues. +# +# The below names agree to abide by the +# [Embargo Policy](https://github.com/kubernetes/sig-release/blob/master/security-release-process-documentation/security-release-process.md#embargo-policy) +# and will be removed and replaced if they violate that agreement. +# +# DO NOT REPORT SECURITY VULNERABILITIES DIRECTLY TO THESE NAMES, FOLLOW THE +# INSTRUCTIONS AT https://kubernetes.io/security/ + +linki +njuettner +hjacobs +raffo From ca0d3aa59ccb21b764071f6bf257de280f23a2cb Mon Sep 17 00:00:00 2001 From: Joakim Olsson Date: Mon, 7 Jan 2019 15:37:22 +0100 Subject: [PATCH 39/44] Add support for eu-north-1 --- provider/aws.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/provider/aws.go b/provider/aws.go index ec14c8d35..fb328cbd2 100644 --- a/provider/aws.go +++ b/provider/aws.go @@ -58,6 +58,7 @@ var ( "eu-west-1.elb.amazonaws.com": "Z32O12XQLNTSW2", "eu-west-2.elb.amazonaws.com": "ZHURV8PSTC4K8", "eu-west-3.elb.amazonaws.com": "Z3Q77PNBQS71R4", + "eu-north-1.elb.amazonaws.com": "Z23TAZ6LKFMNIO", "sa-east-1.elb.amazonaws.com": "Z2P70J7HTTTPLU", // Network Load Balancers "elb.us-east-2.amazonaws.com": "ZLMOA37VPKANP", @@ -74,6 +75,7 @@ var ( "elb.eu-west-1.amazonaws.com": "Z2IFOLAFXWLO4F", "elb.eu-west-2.amazonaws.com": "ZD4D7Y8KGAS4G", "elb.eu-west-3.amazonaws.com": "Z1CMS0P5QUZ6D5", + "elb.eu-north-1.amazonaws.com": "Z1UDT6IFJ4EJM", "elb.sa-east-1.amazonaws.com": "ZTK26PT1VY4CU", } ) From 451bfa879f13ee1b70a11c26d21ddb9cc508f19e Mon Sep 17 00:00:00 2001 From: Zach Yam Date: Mon, 7 Jan 2019 15:11:00 -0500 Subject: [PATCH 40/44] Clarify registry error info --- docs/faq.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index 99a622ea7..8c01b966d 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -159,9 +159,9 @@ You need to add either https://www.googleapis.com/auth/ndev.clouddns.readwrite o ExternalDNS exposes 2 types of metrics: Sources and Registry errors. -`Source`s are mostly Kubernetes API objects. Examples of `source` errors may be connection errors to the Kubernetes API server itself or missing RBAC permissions. It can also stem from incompatible configuration in the objects itself like invalid characters, processing a broken fqdnTemplate etc. +`Source`s are mostly Kubernetes API objects. Examples of `source` errors may be connection errors to the Kubernetes API server itself or missing RBAC permissions. It can also stem from incompatible configuration in the objects itself like invalid characters, processing a broken fqdnTemplate, etc. -`Registry` errors are mostly Provider errors, unless there's some coding flaw in the registry package. Provider errors often arise due to accessing their APIs due to network or missing cloud-provider permissions when reading records. When applying a changeset, errors arise when the changeset applied is incompatible with the current state, in which case ExternalDNS can get stuck forever. +`Registry` errors are mostly Provider errors, unless there's some coding flaw in the registry package. Provider errors often arise due to accessing their APIs due to network or missing cloud-provider permissions when reading records. When applying a changeset, errors will arise if the changeset applied is incompatible with the current state. In case of an increased error count, you could correlate them with the `http_request_duration_seconds{handler="instrumented_http"}` metric which should show increased numbers for status codes 4xx (permissions, configuration, invalid changeset) or 5xx (apiserver down). From f2ed58f00da4086725b54b1bd17ab4803a610cba Mon Sep 17 00:00:00 2001 From: xianlubird Date: Wed, 9 Jan 2019 11:06:36 +0800 Subject: [PATCH 41/44] Fix private zone dns record does not work --- provider/alibaba_cloud.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/provider/alibaba_cloud.go b/provider/alibaba_cloud.go index 79663577d..1bfcd3566 100644 --- a/provider/alibaba_cloud.go +++ b/provider/alibaba_cloud.go @@ -42,6 +42,7 @@ const ( defaultAlibabaCloudPrivateZoneRecordTTL = 60 defaultAlibabaCloudPageSize = 50 nullHostAlibabaCloud = "@" + pVTZDoamin = "pvtz.aliyuncs.com" ) // AlibabaCloudDNSAPI is a minimal implementation of DNS API that we actually use, used primarily for unit testing. @@ -708,6 +709,7 @@ func (p *AlibabaCloudProvider) splitDNSName(endpoint *endpoint.Endpoint) (rr str func (p *AlibabaCloudProvider) matchVPC(zoneID string) bool { request := pvtz.CreateDescribeZoneInfoRequest() request.ZoneId = zoneID + request.Domain = pVTZDoamin response, err := p.getPvtzClient().DescribeZoneInfo(request) if err != nil { log.Errorf("Failed to describe zone info %s in Alibaba Cloud DNS: %v", zoneID, err) @@ -730,7 +732,7 @@ func (p *AlibabaCloudProvider) privateZones() ([]pvtz.Zone, error) { request := pvtz.CreateDescribeZonesRequest() request.PageSize = requests.NewInteger(defaultAlibabaCloudPageSize) request.PageNumber = "1" - + request.Domain = pVTZDoamin for { response, err := p.getPvtzClient().DescribeZones(request) if err != nil { @@ -738,7 +740,7 @@ func (p *AlibabaCloudProvider) privateZones() ([]pvtz.Zone, error) { return nil, err } for _, zone := range response.Zones.Zone { - log.Debugf("Zone: %++v", zone) + log.Infof("PrivateZones zone: %++v", zone) if !p.zoneIDFilter.Match(zone.ZoneId) { continue @@ -784,6 +786,7 @@ func (p *AlibabaCloudProvider) getPrivateZones() (map[string]*alibabaPrivateZone request.ZoneId = zone.ZoneId request.PageSize = requests.NewInteger(defaultAlibabaCloudPageSize) request.PageNumber = "1" + request.Domain = pVTZDoamin var records []pvtz.Record for { @@ -884,6 +887,7 @@ func (p *AlibabaCloudProvider) createPrivateZoneRecord(zones map[string]*alibaba request.ZoneId = zone.ZoneId request.Type = endpoint.RecordType request.Rr = rr + request.Domain = pVTZDoamin ttl := int(endpoint.RecordTTL) if ttl != 0 { @@ -927,6 +931,7 @@ func (p *AlibabaCloudProvider) deletePrivateZoneRecord(recordID int) error { request := pvtz.CreateDeleteZoneRecordRequest() request.RecordId = requests.NewInteger(recordID) + request.Domain = pVTZDoamin response, err := p.getPvtzClient().DeleteZoneRecord(request) if err == nil { @@ -998,6 +1003,7 @@ func (p *AlibabaCloudProvider) updatePrivateZoneRecord(record pvtz.Record, endpo request.Rr = record.Rr request.Type = record.Type request.Value = record.Value + request.Domain = pVTZDoamin ttl := int(endpoint.RecordTTL) if ttl != 0 { request.Ttl = requests.NewInteger(ttl) From 6927af40673110634169c0434daa5df16f47c1ba Mon Sep 17 00:00:00 2001 From: Sheng Lao Date: Sat, 12 Jan 2019 00:06:43 +0800 Subject: [PATCH 42/44] Add apiVersion to ingress.yaml, and Delete the duplicated line in dnstools --- docs/tutorials/coredns.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/coredns.md b/docs/tutorials/coredns.md index e20699c89..de8b16fca 100644 --- a/docs/tutorials/coredns.md +++ b/docs/tutorials/coredns.md @@ -197,6 +197,7 @@ minikube addons enable ingress ## Testing ingress example ``` $ cat ingress.yaml +apiVersion: extensions/v1beta1 kind: Ingress metadata: name: nginx @@ -229,7 +230,6 @@ nginx nginx.example.org 10.0.2.15 80 2m $ 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 -dnstools# dig @10.100.4.143 nginx.example.org +short 10.0.2.15 dnstools# ``` From 7a28e3047a1794feac7087e0016ca6b20ae23e6d Mon Sep 17 00:00:00 2001 From: Joe Hohertz Date: Thu, 17 Jan 2019 12:37:27 -0500 Subject: [PATCH 43/44] Adds a new flag `--aws-api-retries` which allows overriding the number of retries that API calls will attempt before giving up. This somewhat mitigates the issues discussed in #484 by allowing the current sync attempt to complete vs. failing and starting anew. Defaults to 3, which is what the aws-sdk-go defaults to where not specified. Signed-off-by: Joe Hohertz --- main.go | 1 + pkg/apis/externaldns/types.go | 3 +++ pkg/apis/externaldns/types_test.go | 4 ++++ provider/aws.go | 3 ++- 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index 56d4e5777..7bc1f0d4a 100644 --- a/main.go +++ b/main.go @@ -116,6 +116,7 @@ func main() { BatchChangeInterval: cfg.AWSBatchChangeInterval, EvaluateTargetHealth: cfg.AWSEvaluateTargetHealth, AssumeRole: cfg.AWSAssumeRole, + APIRetries: cfg.AWSAPIRetries, DryRun: cfg.DryRun, }, ) diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index 5de4aca5c..8f51cee03 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -61,6 +61,7 @@ type Config struct { AWSBatchChangeSize int AWSBatchChangeInterval time.Duration AWSEvaluateTargetHealth bool + AWSAPIRetries int AzureConfigFile string AzureResourceGroup string CloudflareProxied bool @@ -133,6 +134,7 @@ var defaultConfig = &Config{ AWSBatchChangeSize: 1000, AWSBatchChangeInterval: time.Second, AWSEvaluateTargetHealth: true, + AWSAPIRetries: 3, AzureConfigFile: "/etc/kubernetes/azure.json", AzureResourceGroup: "", CloudflareProxied: false, @@ -248,6 +250,7 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("aws-batch-change-size", "When using the AWS provider, set the maximum number of changes that will be applied in each batch.").Default(strconv.Itoa(defaultConfig.AWSBatchChangeSize)).IntVar(&cfg.AWSBatchChangeSize) app.Flag("aws-batch-change-interval", "When using the AWS provider, set the interval between batch changes.").Default(defaultConfig.AWSBatchChangeInterval.String()).DurationVar(&cfg.AWSBatchChangeInterval) app.Flag("aws-evaluate-target-health", "When using the AWS provider, set whether to evaluate the health of a DNS target (default: enabled, disable with --no-aws-evaluate-target-health)").Default(strconv.FormatBool(defaultConfig.AWSEvaluateTargetHealth)).BoolVar(&cfg.AWSEvaluateTargetHealth) + app.Flag("aws-api-retries", "When using the AWS provider, set the maximum number of retries for API calls before giving up.").Default(strconv.Itoa(defaultConfig.AWSAPIRetries)).IntVar(&cfg.AWSAPIRetries) app.Flag("azure-config-file", "When using the Azure provider, specify the Azure configuration file (required when --provider=azure").Default(defaultConfig.AzureConfigFile).StringVar(&cfg.AzureConfigFile) app.Flag("azure-resource-group", "When using the Azure provider, override the Azure resource group to use (optional)").Default(defaultConfig.AzureResourceGroup).StringVar(&cfg.AzureResourceGroup) app.Flag("cloudflare-proxied", "When using the Cloudflare provider, specify if the proxy mode must be enabled (default: disabled)").BoolVar(&cfg.CloudflareProxied) diff --git a/pkg/apis/externaldns/types_test.go b/pkg/apis/externaldns/types_test.go index 8a1f1854f..7d04abadb 100644 --- a/pkg/apis/externaldns/types_test.go +++ b/pkg/apis/externaldns/types_test.go @@ -48,6 +48,7 @@ var ( AWSBatchChangeSize: 1000, AWSBatchChangeInterval: time.Second, AWSEvaluateTargetHealth: true, + AWSAPIRetries: 3, AzureConfigFile: "/etc/kubernetes/azure.json", AzureResourceGroup: "", CloudflareProxied: false, @@ -100,6 +101,7 @@ var ( AWSBatchChangeSize: 100, AWSBatchChangeInterval: time.Second * 2, AWSEvaluateTargetHealth: false, + AWSAPIRetries: 13, AzureConfigFile: "azure.json", AzureResourceGroup: "arg", CloudflareProxied: true, @@ -195,6 +197,7 @@ func TestParseFlags(t *testing.T) { "--aws-assume-role=some-other-role", "--aws-batch-change-size=100", "--aws-batch-change-interval=2s", + "--aws-api-retries=13", "--no-aws-evaluate-target-health", "--policy=upsert-only", "--registry=noop", @@ -256,6 +259,7 @@ func TestParseFlags(t *testing.T) { "EXTERNAL_DNS_AWS_BATCH_CHANGE_SIZE": "100", "EXTERNAL_DNS_AWS_BATCH_CHANGE_INTERVAL": "2s", "EXTERNAL_DNS_AWS_EVALUATE_TARGET_HEALTH": "0", + "EXTERNAL_DNS_AWS_API_RETRIES": "13", "EXTERNAL_DNS_POLICY": "upsert-only", "EXTERNAL_DNS_REGISTRY": "noop", "EXTERNAL_DNS_TXT_OWNER_ID": "owner-1", diff --git a/provider/aws.go b/provider/aws.go index fb328cbd2..3d5c57409 100644 --- a/provider/aws.go +++ b/provider/aws.go @@ -117,12 +117,13 @@ type AWSConfig struct { BatchChangeInterval time.Duration EvaluateTargetHealth bool AssumeRole string + APIRetries int DryRun bool } // NewAWSProvider initializes a new AWS Route53 based Provider. func NewAWSProvider(awsConfig AWSConfig) (*AWSProvider, error) { - config := aws.NewConfig() + config := aws.NewConfig().WithMaxRetries(awsConfig.APIRetries) config.WithHTTPClient( instrumented_http.NewClient(config.HTTPClient, &instrumented_http.Callbacks{ From 67df440b360b8987fccba661394c7dbb90f8f16a Mon Sep 17 00:00:00 2001 From: Martin Linkhorst Date: Wed, 23 Jan 2019 14:52:26 +0100 Subject: [PATCH 44/44] Add questions from slack to the faq --- docs/faq.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/faq.md b/docs/faq.md index 447cf561d..bcd2240f0 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -243,3 +243,11 @@ To do this with ExternalDNS you can use the `--annotation-filter` to specificall an instance of a ingress controller. Let's assume you have two ingress controllers `nginx-internal` and `nginx-external` then you can start two ExternalDNS providers one with `--annotation-filter=kubernetes.io/ingress.class=nginx-internal` and one with `--annotation-filter=kubernetes.io/ingress.class=nginx-external`. + +### Can external-dns manage(add/remove) records in a hosted zone which is setup in different aws account. + +yes, give it the correct cross-account/assume-role permissions and use the `--aws-assume-role` flag https://github.com/kubernetes-incubator/external-dns/pull/524#issue-181256561 + +### how do I provide multiple values to the annotation `external-dns.alpha.kubernetes.io/hostname` + +separate them by `,`