From aa551ac704085a44ef42667d243256b73eb9212a Mon Sep 17 00:00:00 2001 From: Jonas Michel Date: Wed, 26 Jun 2019 17:20:52 -0500 Subject: [PATCH] Add Contour IngressRoute source implementation --- go.mod | 29 +- go.sum | 85 +++ main.go | 1 + pkg/apis/externaldns/types.go | 7 +- pkg/apis/externaldns/types_test.go | 5 + source/ingressroute.go | 332 +++++++++ source/ingressroute_test.go | 1094 ++++++++++++++++++++++++++++ source/store.go | 60 ++ source/store_test.go | 32 +- 9 files changed, 1618 insertions(+), 27 deletions(-) create mode 100644 source/ingressroute.go create mode 100644 source/ingressroute_test.go diff --git a/go.mod b/go.mod index 4e7a7b05d..f522ea633 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/kubernetes-incubator/external-dns go 1.12 require ( - cloud.google.com/go v0.34.0 + cloud.google.com/go v0.37.4 github.com/Azure/azure-sdk-for-go v10.0.4-beta+incompatible github.com/Azure/go-autorest v10.9.0+incompatible github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 // indirect @@ -25,17 +25,10 @@ require ( github.com/digitalocean/godo v1.1.1 github.com/dnaeon/go-vcr v1.0.1 // indirect github.com/dnsimple/dnsimple-go v0.14.0 - github.com/envoyproxy/go-control-plane v0.6.9 // indirect github.com/exoscale/egoscale v0.11.0 github.com/ffledgling/pdns-go v0.0.0-20180219074714-524e7daccd99 github.com/go-resty/resty v1.8.0 // indirect - github.com/gogo/googleapis v1.1.0 // indirect - github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef // indirect - github.com/golang/mock v1.2.0 // indirect - github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a // indirect github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135 // indirect - github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf // indirect - github.com/googleapis/gnostic v0.2.0 // indirect github.com/gophercloud/gophercloud v0.0.0-20190126172459-c818fa66e4c8 github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c // indirect github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc // indirect @@ -43,7 +36,7 @@ require ( github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect github.com/grpc-ecosystem/grpc-gateway v1.8.5 // indirect github.com/hashicorp/go-multierror v1.0.0 // indirect - github.com/imdario/mergo v0.3.5 // indirect + github.com/heptio/contour v0.13.0 github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/infobloxopen/infoblox-go-client v0.0.0-20180606155407-61dc5f9b0a65 github.com/jonboulle/clockwork v0.1.0 // indirect @@ -54,7 +47,6 @@ require ( github.com/mattn/go-isatty v0.0.7 // indirect github.com/miekg/dns v1.0.8 github.com/mitchellh/mapstructure v1.1.2 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.1 // indirect github.com/natefinch/lumberjack v2.0.0+incompatible // indirect github.com/nesv/go-dynect v0.6.0 @@ -62,18 +54,16 @@ require ( github.com/onsi/ginkgo v1.8.0 // indirect github.com/onsi/gomega v1.5.0 // indirect github.com/oracle/oci-go-sdk v1.8.0 - github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pkg/errors v0.8.1 github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829 github.com/sanyu/dynectsoap v0.0.0-20181203081243-b83de5edc4e0 github.com/satori/go.uuid v1.2.0 // indirect github.com/sergi/go-diff v1.0.0 // indirect - github.com/sirupsen/logrus v1.2.0 + github.com/sirupsen/logrus v1.4.1 github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a // indirect github.com/soheilhy/cmux v0.1.3 // indirect github.com/spf13/cobra v0.0.3 // indirect - github.com/spf13/pflag v1.0.2 // indirect github.com/stretchr/testify v1.2.2 github.com/tent/http-link-go v0.0.0-20130702225549-ac974c61c2f9 // indirect github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8 // indirect @@ -86,19 +76,18 @@ require ( go.uber.org/atomic v1.3.2 // indirect go.uber.org/multierr v1.1.0 // indirect go.uber.org/zap v1.9.1 // indirect - golang.org/x/net v0.0.0-20190311183353-d8887717615a - golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 - google.golang.org/api v0.3.0 + golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c + golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a + google.golang.org/api v0.3.1 google.golang.org/appengine v1.5.0 // indirect - gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/ns1/ns1-go.v2 v2.0.0-20190322154155-0dafb5275fd1 gopkg.in/yaml.v2 v2.2.2 istio.io/api v0.0.0-20190321180614-db16d82d3672 istio.io/istio v0.0.0-20190322063008-2b1331886076 - k8s.io/api v0.0.0-20180628040859-072894a440bd - k8s.io/apiextensions-apiserver v0.0.0-20180628053655-3de98c57bc05 // indirect - k8s.io/apimachinery v0.0.0-20180621070125-103fd098999d + k8s.io/api v0.0.0-20190503184017-f1b257a4ce96 + k8s.io/apiextensions-apiserver v0.0.0-20190503184539-c338b28ceaa1 + k8s.io/apimachinery v0.0.0-20190223001710-c182ff3b9841 k8s.io/client-go v8.0.0+incompatible k8s.io/kube-openapi v0.0.0-20190401085232-94e1e7b7574c // indirect launchpad.net/gocheck v0.0.0-20140225173054-000000000087 // indirect diff --git a/go.sum b/go.sum index 405476f32..4f721f299 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.37.4 h1:glPeL3BQJsbF6aIIYfZizMwc5LTYz250bDMjttbBGAU= +cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw= code.cloudfoundry.org/gofileutils v0.0.0-20170111115228-4d0c80011a0f h1:UrKzEwTgeiff9vxdrfdqxibzpWjxLnuXDI5m6z3GJAk= code.cloudfoundry.org/gofileutils v0.0.0-20170111115228-4d0c80011a0f/go.mod h1:sk5LnIjB/nIEU7yP5sDQExVm62wu0pBh3yrElngUisI= git.apache.org/thrift.git v0.12.0/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= @@ -70,6 +72,11 @@ github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1 github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/envoyproxy/go-control-plane v0.6.9 h1:deEH9W8ZAUGNbCdX+9iNzBOGrAOrnpJGoy0PcTqk/tE= github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= +github.com/envoyproxy/go-control-plane v0.8.0 h1:uE6Fp4fOcAJdc1wTQXLJ+SYistkbG1dNoi6Zs1+Ybvk= +github.com/envoyproxy/go-control-plane v0.8.0/go.mod h1:GSSbY9P1neVhdY7G4wu+IK1rk/dqhiCC/4ExuWJZVuk= +github.com/envoyproxy/protoc-gen-validate v0.0.14 h1:YBW6/cKy9prEGRYLnaGa4IDhzxZhRCtKsax8srGKDnM= +github.com/envoyproxy/protoc-gen-validate v0.0.14/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v4.1.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/exoscale/egoscale v0.11.0 h1:g+UBsxLDouKWW2BK/UTgQFAVnM2aHygheF0Dxj0ycC8= github.com/exoscale/egoscale v0.11.0/go.mod h1:Ee3U4ZjSDpbbEc9VkQ/jttUU8USE8Nv7L3YzVi03Y1U= github.com/ffledgling/pdns-go v0.0.0-20180219074714-524e7daccd99 h1:jmwW6QWvUO2OPe22YfgFvBaaZlSr8Rlrac5lZvG6IdM= @@ -90,6 +97,8 @@ github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFG github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0 h1:xU6/SpYbvkNYiptHJYEDRseDLvYE7wSqhYYNy0QSUzI= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef h1:veQD95Isof8w9/WXiA+pa3tz3fJXkt5B7QaRBrM62gk= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= @@ -100,12 +109,17 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a h1:ZJu5NB1Bk5ms4vw0Xu4i+jD32SE9jQXyfnOvwhHqlT0= github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135 h1:zLTLjkaOFEFIOxY5BWLFLwh+cL8vOBW4XJ2aqLE/Tf0= github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeqr2z77+8R2RKyh8PG66dcu1V0ck= github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gnostic v0.2.0 h1:l6N3VoaVzTncYYW+9yOz2LJJammFZGBO13sqgEhpy9g= github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/gophercloud/gophercloud v0.0.0-20190126172459-c818fa66e4c8 h1:L9JPKrtsHMQ4VCRQfHvbbHBfB2Urn8xf6QZeXZ+OrN4= @@ -113,12 +127,14 @@ github.com/gophercloud/gophercloud v0.0.0-20190126172459-c818fa66e4c8/go.mod h1: github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gordonklaus/ineffassign v0.0.0-20180909121442-1003c8bd00dc/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU= github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c h1:Lh2aW+HnU2Nbe1gqD9SOJLJxW1jBMmQOktN2acDyJk8= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gregjones/httpcache v0.0.0-20181110185634-c63ab54fda8f/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc h1:f8eY6cV/x1x+HLjOp4r72s/31/V2aTUtg5oKRRPf8/Q= github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v0.0.0-20190222133341-cfaf5686ec79 h1:lR9ssWAqp9qL0bALxqEEkuudiP1eweOdv9jsRK3e7lE= @@ -134,10 +150,16 @@ github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uP github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/heptio/contour v0.13.0 h1:kiJ7qC439vFD21DTyQPNPzOGfDqkO/y//GrLYWJ5MdY= +github.com/heptio/contour v0.13.0/go.mod h1:qYE0FAuA8W1NJEaHmyOoWkJZTRPL1x4GVO5BGZMG1Os= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/imdario/mergo v0.3.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q= github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI= +github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/infobloxopen/infoblox-go-client v0.0.0-20180606155407-61dc5f9b0a65 h1:FP5rOFP4ifbtFIjFHJmwhFrsbDyONILK/FNntl/Pou8= @@ -146,12 +168,16 @@ github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5i github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -176,12 +202,14 @@ github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mdempsky/unconvert v0.0.0-20190325185700-2f5dc3378ed3/go.mod h1:9+3Wp2ccIz73BJqVfc7n2+1A+mzvnEwtDTqEjeRngBQ= github.com/miekg/dns v1.0.8 h1:Zi8HNpze3NeRWH1PQV6O71YcvJRQ6j0lORO6DAEmAAI= github.com/miekg/dns v1.0.8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= @@ -220,13 +248,18 @@ github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f h1:BVwpUVJDADN2ufcGik7W992pyps0wZ888b/y9GXcLTU= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.2.0 h1:kUZDBDTdBVBYBj5Tmh2NZLlF60mfjA27rM34b+cVwNU= github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1 h1:/K3IL0Z1quvmJ7X0A1AwNEK7CRkVK3YwfOU/QAL4WGg= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190403104016-ea9eea638872 h1:0aNv3xC7DmQoy1/x1sMh18g+fihWW68LL13i8ao9kl4= +github.com/prometheus/procfs v0.0.0-20190403104016-ea9eea638872/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.2.1/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/sanyu/dynectsoap v0.0.0-20181203081243-b83de5edc4e0 h1:vOcHdR1nu7DO4BAx1rwzdHV7jQTzW3gqcBT5qxHSc6A= github.com/sanyu/dynectsoap v0.0.0-20181203081243-b83de5edc4e0/go.mod h1:FeplEtXXejBYC4NPAFTrs5L7KuK+5RL9bf5nB2vZe9o= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= @@ -235,6 +268,8 @@ github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/smartystreets/assertions v0.0.0-20180725160413-e900ae048470/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= @@ -249,6 +284,8 @@ github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/pflag v1.0.2 h1:Fy0orTDgHdbnzHcsOgfCN4LtHf0ec3wwtiwJqwvf3Gc= github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= @@ -274,6 +311,8 @@ go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opencensus.io v0.19.1/go.mod h1:gug0GbSHa8Pafr0d2urOSgoXHZ6x/RUlaiT0d9pqb4A= go.opencensus.io v0.19.2 h1:ZZpq6xI6kv/LuE/5s5UQvBU5vMjvRnPb8PvJrIntAnc= go.opencensus.io v0.19.2/go.mod h1:NO/8qkisMZLZ1FCsKNqtJPwc8/TaclWyY0B6wcYNg9M= +go.opencensus.io v0.20.1 h1:pMEjRZ1M4ebWGikflH7nQpV6+Zr88KBMA2XJD3sbijw= +go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= @@ -283,6 +322,8 @@ go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5 h1:bselrhR0Or1vomJZC8ZIjWtbDmn9OYFLX5Ik9alpJpE= +golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -302,17 +343,22 @@ golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c h1:uOCk1iQW6Vc18bnC13MfzScl+wdKBmM9Y9kU7Z83/lw= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190130055435-99b60b757ec1/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 h1:Wo7BWFiOk0QRFMLYMqJGFMd9CgUAcGx7V+qEg/h5IBI= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a h1:tImsplftrFpALCYumobsd0K86vlAs/eXGFms2txfJfA= +golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 h1:bjcUS9ztw9kFmmIxJInhon/0Is3p+EHBKNgquIzo1OI= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -323,20 +369,35 @@ golang.org/x/sys v0.0.0-20181218192612-074acd46bca6/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190508220229-2d0786266e9c h1:hDn6jm7snBX2O7+EeTk6Q4WXJfKt7MWgtiCCRi1rBoY= +golang.org/x/sys v0.0.0-20190508220229-2d0786266e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 h1:+DCIGbF/swA92ohVg0//6X2IVY3KZs6p9mix0ziNYJM= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuArfcOvC4AoJmILihzhDg= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181219222714-6e267b5cc78e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190213192042-740235f6c0d8/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190325161752-5a8dccf5b48a/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= google.golang.org/api v0.0.0-20181220000619-583d854617af/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.2.0/go.mod h1:IfRCZScioGtypHNTlz3gFk67J8uePVW7uDTBzXuIkhU= google.golang.org/api v0.3.0 h1:UIJY20OEo3+tK5MBlcdx37kmdH6EnRjGkW78mc6+EeA= google.golang.org/api v0.3.0/go.mod h1:IuvZyQh8jgscv8qWfQ4ABd8m7hEudgBFM/EdhA3BnXw= +google.golang.org/api v0.3.1 h1:oJra/lMfmtm13/rgY/8i3MzjFWYXvQIAKjQ3HqofMk8= +google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -346,14 +407,19 @@ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoA google.golang.org/genproto v0.0.0-20181219182458-5a97ab628bfb/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19 h1:Lj2SnHtxkRGJDqnGaSjo+CCdIieEnwVazbOXILwQemk= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107 h1:xtNn7qFlagY2mQNFHMSRPjT2RkOV4OXM7P5TVy9xATo= +google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0 h1:cfg4PD8YEdSFnm7qLV4++93WcmhH2nIUhMjhdCvl3j8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.19.1 h1:TrBcJ1yqAl1G++wO39nD/qtgpsW9/1+QGrluyMGEYgM= +google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= @@ -373,19 +439,38 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20180920025451-e3ad64cb4ed3/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= istio.io/api v0.0.0-20190321180614-db16d82d3672 h1:luY97pBVarSo1v++zf2kgb84Q55G5hv/ult2A4KPQuk= istio.io/api v0.0.0-20190321180614-db16d82d3672/go.mod h1:hhLFQmpHia8zgaM37vb2ml9iS5NfNfqZGRt1pS9aVEo= +istio.io/gogo-genproto v0.0.0-20190124151557-6d926a6e6feb/go.mod h1:eIDJ6jNk/IeJz6ODSksHl5Aiczy5JUq6vFhJWI5OtiI= istio.io/istio v0.0.0-20190322063008-2b1331886076 h1:gZhCrmVzfQJoDl4oav8i5+NF7p7v0M1Pou+2O+hZBtc= istio.io/istio v0.0.0-20190322063008-2b1331886076/go.mod h1:OWBySrQjjk549IhxWCt7DTl9ZSsXdvbgm+SmgGVRsGA= k8s.io/api v0.0.0-20180628040859-072894a440bd h1:HzgYeLDS1jLxw8DGr68KJh9cdQ5iZJizG0HZWstIhfQ= k8s.io/api v0.0.0-20180628040859-072894a440bd/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA= +k8s.io/api v0.0.0-20190226173710-145d52631d00 h1:xYfyMq0qxTGAg3O9GK23GMbNrBcpnFg9IeA6isDgIXk= +k8s.io/api v0.0.0-20190226173710-145d52631d00/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA= +k8s.io/api v0.0.0-20190503184017-f1b257a4ce96 h1:zq/7PZXqJ6ZbPfLRbIm9Qs6gHMviY72SPk4ugPUPDvI= +k8s.io/api v0.0.0-20190503184017-f1b257a4ce96/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA= k8s.io/apiextensions-apiserver v0.0.0-20180628053655-3de98c57bc05 h1:uKDX+1GgQuV/J6TTgrtHYGRFZUPWxC13mJwBhjIhm/w= k8s.io/apiextensions-apiserver v0.0.0-20180628053655-3de98c57bc05/go.mod h1:IxkesAMoaCRoLrPJdZNZUQp9NfZnzqaVzLhb2VEQzXE= +k8s.io/apiextensions-apiserver v0.0.0-20190503184539-c338b28ceaa1 h1:uNrdMFGXgDAaw+WyJSuRhnzW2eZkqZjc04SZOr4wky8= +k8s.io/apiextensions-apiserver v0.0.0-20190503184539-c338b28ceaa1/go.mod h1:IxkesAMoaCRoLrPJdZNZUQp9NfZnzqaVzLhb2VEQzXE= k8s.io/apimachinery v0.0.0-20180621070125-103fd098999d h1:MZjlsu9igBoVPZkXpIGoxI6EonqNsXXZU7hhvfQLkd4= k8s.io/apimachinery v0.0.0-20180621070125-103fd098999d/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0= +k8s.io/apimachinery v0.0.0-20190221084156-01f179d85dbc h1:7z9/6jKWBqkK9GI1RRB0B5fZcmkatLQ/nv8kysch24o= +k8s.io/apimachinery v0.0.0-20190221084156-01f179d85dbc/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0= +k8s.io/apimachinery v0.0.0-20190223001710-c182ff3b9841 h1:Q4RZrHNtlC/mSdC1sTrcZ5RchC/9vxLVj57pWiCBKv4= +k8s.io/apimachinery v0.0.0-20190223001710-c182ff3b9841/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0= +k8s.io/client-go v0.0.0-20190226174127-78295b709ec6/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s= k8s.io/client-go v8.0.0+incompatible h1:tTI4hRmb1DRMl4fG6Vclfdi6nTM82oIrTT7HfitmxC4= k8s.io/client-go v8.0.0+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s= +k8s.io/code-generator v0.0.0-20190409092313-b1289fc74931/go.mod h1:MYiN+ZJZ9HkETbgVZdWw2AsuAi9PZ4V80cwfuf2axe8= +k8s.io/gengo v0.0.0-20190116091435-f8a0810f38af/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/klog v0.0.0-20190306015804-8e90cee79f82/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/kube-openapi v0.0.0-20190115222348-ced9eb3070a5/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc= k8s.io/kube-openapi v0.0.0-20190401085232-94e1e7b7574c h1:kJCzg2vGCzah5icgkKR7O1Dzn0NA2iGlym27sb0ZfGE= k8s.io/kube-openapi v0.0.0-20190401085232-94e1e7b7574c/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc= launchpad.net/gocheck v0.0.0-20140225173054-000000000087 h1:Izowp2XBH6Ya6rv+hqbceQyw/gSGoXfH/UPoTGduL54= launchpad.net/gocheck v0.0.0-20140225173054-000000000087/go.mod h1:hj7XX3B/0A+80Vse0e+BUHsHMTEhd0O4cpUHr/e/BUM= +mvdan.cc/unparam v0.0.0-20190310220240-1b9ccfa71afe/go.mod h1:BnhuWBAqxH3+J5bDybdxgw5ZfS+DsVd4iylsKQePN8o= diff --git a/main.go b/main.go index 5c6737f7e..58c287105 100644 --- a/main.go +++ b/main.go @@ -85,6 +85,7 @@ func main() { CFAPIEndpoint: cfg.CFAPIEndpoint, CFUsername: cfg.CFUsername, CFPassword: cfg.CFPassword, + ContourLoadBalancerService: cfg.ContourLoadBalancerService, } // Lookup all the selected sources by names and pass them the desired configuration. diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index 2107d746c..b0f498418 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -41,6 +41,7 @@ type Config struct { KubeConfig string RequestTimeout time.Duration IstioIngressGatewayServices []string + ContourLoadBalancerService string Sources []string Namespace string AnnotationFilter string @@ -129,6 +130,7 @@ var defaultConfig = &Config{ KubeConfig: "", RequestTimeout: time.Second * 30, IstioIngressGatewayServices: []string{"istio-system/istio-ingressgateway"}, + ContourLoadBalancerService: "heptio-contour/contour", Sources: nil, Namespace: "", AnnotationFilter: "", @@ -260,8 +262,11 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("cf-username", "The username to log into the cloud foundry API").Default(defaultConfig.CFUsername).StringVar(&cfg.CFUsername) app.Flag("cf-password", "The password to log into the cloud foundry API").Default(defaultConfig.CFPassword).StringVar(&cfg.CFPassword) + // Flags related to Contour + app.Flag("contour-load-balancer", "The fully-qualified name of the Contour load balancer service. (default: heptio-contour/contour)").Default("heptio-contour/contour").StringVar(&cfg.ContourLoadBalancerService) + // Flags related to processing sources - app.Flag("source", "The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress, fake, connector, istio-gateway, cloudfoundry, crd, empty").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress", "istio-gateway", "cloudfoundry", "fake", "connector", "crd", "empty") + app.Flag("source", "The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress, fake, connector, istio-gateway, cloudfoundry, contour-ingressroute, crd, empty").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress", "istio-gateway", "cloudfoundry", "contour-ingressroute", "fake", "connector", "crd", "empty") app.Flag("namespace", "Limit sources of endpoints to a specific namespace (default: all namespaces)").Default(defaultConfig.Namespace).StringVar(&cfg.Namespace) app.Flag("annotation-filter", "Filter sources managed by external-dns via annotation using label selector semantics (default: all sources)").Default(defaultConfig.AnnotationFilter).StringVar(&cfg.AnnotationFilter) app.Flag("fqdn-template", "A templated string that's used to generate DNS names from sources that don't define a hostname themselves, or to add a hostname suffix when paired with the fake source (optional). Accepts comma separated list for multiple global FQDN.").Default(defaultConfig.FQDNTemplate).StringVar(&cfg.FQDNTemplate) diff --git a/pkg/apis/externaldns/types_test.go b/pkg/apis/externaldns/types_test.go index dc580a036..0eb4cb4b7 100644 --- a/pkg/apis/externaldns/types_test.go +++ b/pkg/apis/externaldns/types_test.go @@ -33,6 +33,7 @@ var ( KubeConfig: "", RequestTimeout: time.Second * 30, IstioIngressGatewayServices: []string{"istio-system/istio-ingressgateway"}, + ContourLoadBalancerService: "heptio-contour/contour", Sources: []string{"service"}, Namespace: "", FQDNTemplate: "", @@ -93,6 +94,7 @@ var ( KubeConfig: "/some/path", RequestTimeout: time.Second * 77, IstioIngressGatewayServices: []string{"istio-other/istio-otheringressgateway"}, + ContourLoadBalancerService: "heptio-contour-other/contour-other", Sources: []string{"service", "ingress", "connector"}, Namespace: "namespace", IgnoreHostnameAnnotation: true, @@ -162,6 +164,7 @@ var ( RequestTimeout: time.Second * 30, IstioIngressGatewayServices: []string{"istio-system/istio-ingressgateway", "istio-other/istio-otheringressgateway"}, Sources: []string{"istio-gateway"}, + ContourLoadBalancerService: "heptio-contour/contour", Namespace: "", FQDNTemplate: "", Compatibility: "", @@ -237,6 +240,7 @@ func TestParseFlags(t *testing.T) { "--kubeconfig=/some/path", "--request-timeout=77s", "--istio-ingress-gateway=istio-other/istio-otheringressgateway", + "--contour-load-balancer=heptio-contour-other/contour-other", "--source=service", "--source=ingress", "--source=connector", @@ -314,6 +318,7 @@ func TestParseFlags(t *testing.T) { "EXTERNAL_DNS_KUBECONFIG": "/some/path", "EXTERNAL_DNS_REQUEST_TIMEOUT": "77s", "EXTERNAL_DNS_ISTIO_INGRESS_GATEWAY": "istio-other/istio-otheringressgateway", + "EXTERNAL_DNS_CONTOUR_LOAD_BALANCER": "heptio-contour-other/contour-other", "EXTERNAL_DNS_SOURCE": "service\ningress\nconnector", "EXTERNAL_DNS_NAMESPACE": "namespace", "EXTERNAL_DNS_FQDN_TEMPLATE": "{{.Name}}.service.example.com", diff --git a/source/ingressroute.go b/source/ingressroute.go new file mode 100644 index 000000000..036846ef0 --- /dev/null +++ b/source/ingressroute.go @@ -0,0 +1,332 @@ +/* +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 source + +import ( + "bytes" + "fmt" + "sort" + "strings" + "text/template" + "time" + + contourapi "github.com/heptio/contour/apis/contour/v1beta1" + contour "github.com/heptio/contour/apis/generated/clientset/versioned" + contourinformers "github.com/heptio/contour/apis/generated/informers/externalversions" + extinformers "github.com/heptio/contour/apis/generated/informers/externalversions/contour/v1beta1" + "github.com/kubernetes-incubator/external-dns/endpoint" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/cache" +) + +// ingressRouteSource is an implementation of Source for Heptio Contour IngressRoute objects. +// The IngressRoute implementation uses the spec.virtualHost.fqdn value for the hostname. +// Use targetAnnotationKey to explicitly set Endpoint. +type ingressRouteSource struct { + kubeClient kubernetes.Interface + contourClient contour.Interface + contourLoadBalancerService string + namespace string + annotationFilter string + fqdnTemplate *template.Template + combineFQDNAnnotation bool + ignoreHostnameAnnotation bool + ingressRouteInformer extinformers.IngressRouteInformer +} + +// NewIngressRouteSource creates a new ingressRouteSource with the given config. +func NewContourIngressRouteSource( + kubeClient kubernetes.Interface, + contourClient contour.Interface, + contourLoadBalancerService string, + namespace string, + annotationFilter string, + fqdnTemplate string, + combineFqdnAnnotation bool, + ignoreHostnameAnnotation bool, +) (Source, error) { + var ( + tmpl *template.Template + err error + ) + if fqdnTemplate != "" { + tmpl, err = template.New("endpoint").Funcs(template.FuncMap{ + "trimPrefix": strings.TrimPrefix, + }).Parse(fqdnTemplate) + if err != nil { + return nil, err + } + } + + if _, _, err = parseContourLoadBalancerService(contourLoadBalancerService); err != nil { + return nil, err + } + + // Use shared informer to listen for add/update/delete of ingressroutes in the specified namespace. + // Set resync period to 0, to prevent processing when nothing has changed. + informerFactory := contourinformers.NewSharedInformerFactoryWithOptions( + contourClient, + 0, + contourinformers.WithNamespace(namespace), + ) + ingressRouteInformer := informerFactory.Contour().V1beta1().IngressRoutes() + + // Add default resource event handlers to properly initialize informer. + ingressRouteInformer.Informer().AddEventHandler( + cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + }, + }, + ) + + // TODO informer is not explicitly stopped since controller is not passing in its channel. + informerFactory.Start(wait.NeverStop) + + // wait for the local cache to be populated. + err = wait.Poll(time.Second, 60*time.Second, func() (bool, error) { + return ingressRouteInformer.Informer().HasSynced() == true, nil + }) + if err != nil { + return nil, fmt.Errorf("failed to sync cache: %v", err) + } + + return &ingressRouteSource{ + kubeClient: kubeClient, + contourClient: contourClient, + contourLoadBalancerService: contourLoadBalancerService, + namespace: namespace, + annotationFilter: annotationFilter, + fqdnTemplate: tmpl, + combineFQDNAnnotation: combineFqdnAnnotation, + ignoreHostnameAnnotation: ignoreHostnameAnnotation, + ingressRouteInformer: ingressRouteInformer, + }, nil +} + +// Endpoints returns endpoint objects for each host-target combination that should be processed. +// Retrieves all ingressroute resources in the source's namespace(s). +func (sc *ingressRouteSource) Endpoints() ([]*endpoint.Endpoint, error) { + irs, err := sc.ingressRouteInformer.Lister().IngressRoutes(sc.namespace).List(labels.Everything()) + if err != nil { + return nil, err + } + + irs, err = sc.filterByAnnotations(irs) + if err != nil { + return nil, err + } + + endpoints := []*endpoint.Endpoint{} + + for _, ir := range irs { + // Check controller annotation to see if we are responsible. + controller, ok := ir.Annotations[controllerAnnotationKey] + if ok && controller != controllerAnnotationValue { + log.Debugf("Skipping ingressroute %s/%s because controller value does not match, found: %s, required: %s", + ir.Namespace, ir.Name, controller, controllerAnnotationValue) + continue + } else if ir.CurrentStatus != "valid" { + log.Debugf("Skipping ingressroute %s/%s because it is not valid", ir.Namespace, ir.Name) + continue + } + + irEndpoints, err := sc.endpointsFromIngressRoute(ir) + if err != nil { + return nil, err + } + + // apply template if fqdn is missing on ingressroute + if (sc.combineFQDNAnnotation || len(irEndpoints) == 0) && sc.fqdnTemplate != nil { + tmplEndpoints, err := sc.endpointsFromTemplate(ir) + if err != nil { + return nil, err + } + + if sc.combineFQDNAnnotation { + irEndpoints = append(irEndpoints, tmplEndpoints...) + } else { + irEndpoints = tmplEndpoints + } + } + + if len(irEndpoints) == 0 { + log.Debugf("No endpoints could be generated from ingressroute %s/%s", ir.Namespace, ir.Name) + continue + } + + log.Debugf("Endpoints generated from ingressroute: %s/%s: %v", ir.Namespace, ir.Name, irEndpoints) + sc.setResourceLabel(ir, irEndpoints) + endpoints = append(endpoints, irEndpoints...) + } + + for _, ep := range endpoints { + sort.Sort(ep.Targets) + } + + return endpoints, nil +} + +func (sc *ingressRouteSource) endpointsFromTemplate(ingressRoute *contourapi.IngressRoute) ([]*endpoint.Endpoint, error) { + // Process the whole template string + var buf bytes.Buffer + err := sc.fqdnTemplate.Execute(&buf, ingressRoute) + if err != nil { + return nil, fmt.Errorf("failed to apply template on ingressroute %s/%s: %v", ingressRoute.Namespace, ingressRoute.Name, err) + } + + hostnames := buf.String() + + ttl, err := getTTLFromAnnotations(ingressRoute.Annotations) + if err != nil { + log.Warn(err) + } + + targets := getTargetsFromTargetAnnotation(ingressRoute.Annotations) + + if len(targets) == 0 { + targets, err = sc.targetsFromContourLoadBalancer() + if err != nil { + return nil, err + } + } + + providerSpecific := getProviderSpecificAnnotations(ingressRoute.Annotations) + + var endpoints []*endpoint.Endpoint + // splits the FQDN template and removes the trailing periods + hostnameList := strings.Split(strings.Replace(hostnames, " ", "", -1), ",") + for _, hostname := range hostnameList { + hostname = strings.TrimSuffix(hostname, ".") + endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific)...) + } + return endpoints, nil +} + +// filterByAnnotations filters a list of configs by a given annotation selector. +func (sc *ingressRouteSource) filterByAnnotations(ingressRoutes []*contourapi.IngressRoute) ([]*contourapi.IngressRoute, error) { + labelSelector, err := metav1.ParseToLabelSelector(sc.annotationFilter) + if err != nil { + return nil, err + } + selector, err := metav1.LabelSelectorAsSelector(labelSelector) + if err != nil { + return nil, err + } + + // empty filter returns original list + if selector.Empty() { + return ingressRoutes, nil + } + + filteredList := []*contourapi.IngressRoute{} + + for _, ingressRoute := range ingressRoutes { + // convert the ingressroute's annotations to an equivalent label selector + annotations := labels.Set(ingressRoute.Annotations) + + // include ingressroute if its annotations match the selector + if selector.Matches(annotations) { + filteredList = append(filteredList, ingressRoute) + } + } + + return filteredList, nil +} + +func (sc *ingressRouteSource) setResourceLabel(ingressRoute *contourapi.IngressRoute, endpoints []*endpoint.Endpoint) { + for _, ep := range endpoints { + ep.Labels[endpoint.ResourceLabelKey] = fmt.Sprintf("ingressroute/%s/%s", ingressRoute.Namespace, ingressRoute.Name) + } +} + +func (sc *ingressRouteSource) targetsFromContourLoadBalancer() (targets endpoint.Targets, err error) { + lbNamespace, lbName, err := parseContourLoadBalancerService(sc.contourLoadBalancerService) + if err != nil { + return nil, err + } + if svc, err := sc.kubeClient.CoreV1().Services(lbNamespace).Get(lbName, metav1.GetOptions{}); err != nil { + log.Warn(err) + } else { + for _, lb := range svc.Status.LoadBalancer.Ingress { + if lb.IP != "" { + targets = append(targets, lb.IP) + } + if lb.Hostname != "" { + targets = append(targets, lb.Hostname) + } + } + } + + return +} + +// endpointsFromIngressRouteConfig extracts the endpoints from a Contour IngressRoute object +func (sc *ingressRouteSource) endpointsFromIngressRoute(ingressRoute *contourapi.IngressRoute) ([]*endpoint.Endpoint, error) { + if ingressRoute.CurrentStatus != "valid" { + log.Warn(errors.Errorf("cannot generate endpoints for ingressroute with status %s", ingressRoute.CurrentStatus)) + return nil, nil + } + + var endpoints []*endpoint.Endpoint + + ttl, err := getTTLFromAnnotations(ingressRoute.Annotations) + if err != nil { + log.Warn(err) + } + + targets := getTargetsFromTargetAnnotation(ingressRoute.Annotations) + + if len(targets) == 0 { + targets, err = sc.targetsFromContourLoadBalancer() + if err != nil { + return nil, err + } + } + + providerSpecific := getProviderSpecificAnnotations(ingressRoute.Annotations) + + host := ingressRoute.Spec.VirtualHost.Fqdn + if host != "" { + endpoints = append(endpoints, endpointsForHostname(host, targets, ttl, providerSpecific)...) + } + + // Skip endpoints if we do not want entries from annotations + if !sc.ignoreHostnameAnnotation { + hostnameList := getHostnamesFromAnnotations(ingressRoute.Annotations) + for _, hostname := range hostnameList { + endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific)...) + } + } + + return endpoints, nil +} + +func parseContourLoadBalancerService(service string) (namespace, name string, err error) { + parts := strings.Split(service, "/") + if len(parts) != 2 { + err = fmt.Errorf("invalid contour load balancer service (namespace/name) found '%v'", service) + } else { + namespace, name = parts[0], parts[1] + } + + return +} diff --git a/source/ingressroute_test.go b/source/ingressroute_test.go new file mode 100644 index 000000000..8edcc1d97 --- /dev/null +++ b/source/ingressroute_test.go @@ -0,0 +1,1094 @@ +/* +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 source + +import ( + "testing" + + contour "github.com/heptio/contour/apis/contour/v1beta1" + fakeContour "github.com/heptio/contour/apis/generated/clientset/versioned/fake" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + fakeKube "k8s.io/client-go/kubernetes/fake" + + "github.com/kubernetes-incubator/external-dns/endpoint" +) + +// This is a compile-time validation that ingressRouteSource is a Source. +var _ Source = &ingressRouteSource{} + +type IngressRouteSuite struct { + suite.Suite + source Source + loadBalancer *v1.Service + ingressRoute *contour.IngressRoute +} + +func (suite *IngressRouteSuite) SetupTest() { + fakeKubernetesClient := fakeKube.NewSimpleClientset() + fakeContourClient := fakeContour.NewSimpleClientset() + var err error + + suite.loadBalancer = (fakeLoadBalancerService{ + ips: []string{"8.8.8.8"}, + hostnames: []string{"v1"}, + namespace: "heptio-contour/contour", + name: "contour", + }).Service() + + _, err = fakeKubernetesClient.CoreV1().Services(suite.loadBalancer.Namespace).Create(suite.loadBalancer) + suite.NoError(err, "should succeed") + + suite.source, err = NewContourIngressRouteSource( + fakeKubernetesClient, + fakeContourClient, + "heptio-contour/contour", + "default", + "", + "{{.Name}}", + false, + false, + ) + suite.NoError(err, "should initialize ingressroute source") + + suite.ingressRoute = (fakeIngressRoute{ + name: "foo-ingressroute-with-targets", + namespace: "default", + host: "example.com", + }).IngressRoute() + _, err = fakeContourClient.ContourV1beta1().IngressRoutes(suite.ingressRoute.Namespace).Create(suite.ingressRoute) + suite.NoError(err, "should succeed") +} + +func (suite *IngressRouteSuite) TestResourceLabelIsSet() { + endpoints, _ := suite.source.Endpoints() + for _, ep := range endpoints { + suite.Equal("ingressroute/default/foo-ingressroute-with-targets", ep.Labels[endpoint.ResourceLabelKey], "should set correct resource label") + } +} + +func TestIngressRoute(t *testing.T) { + suite.Run(t, new(IngressRouteSuite)) + t.Run("endpointsFromIngressRoute", testEndpointsFromIngressRoute) + t.Run("Endpoints", testIngressRouteEndpoints) +} + +func TestNewContourIngressRouteSource(t *testing.T) { + for _, ti := range []struct { + title string + annotationFilter string + fqdnTemplate string + combineFQDNAndAnnotation bool + expectError bool + }{ + { + title: "invalid template", + expectError: true, + fqdnTemplate: "{{.Name", + }, + { + title: "valid empty template", + expectError: false, + }, + { + title: "valid template", + expectError: false, + fqdnTemplate: "{{.Name}}-{{.Namespace}}.ext-dns.test.com", + }, + { + title: "valid template", + expectError: false, + fqdnTemplate: "{{.Name}}-{{.Namespace}}.ext-dns.test.com, {{.Name}}-{{.Namespace}}.ext-dna.test.com", + }, + { + title: "valid template", + expectError: false, + fqdnTemplate: "{{.Name}}-{{.Namespace}}.ext-dns.test.com, {{.Name}}-{{.Namespace}}.ext-dna.test.com", + combineFQDNAndAnnotation: true, + }, + { + title: "non-empty annotation filter label", + expectError: false, + annotationFilter: "contour.heptio.com/ingress.class=contour", + }, + } { + t.Run(ti.title, func(t *testing.T) { + _, err := NewContourIngressRouteSource( + fakeKube.NewSimpleClientset(), + fakeContour.NewSimpleClientset(), + "heptio-contour/contour", + "", + ti.annotationFilter, + ti.fqdnTemplate, + ti.combineFQDNAndAnnotation, + false, + ) + if ti.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func testEndpointsFromIngressRoute(t *testing.T) { + for _, ti := range []struct { + title string + loadBalancer fakeLoadBalancerService + ingressRoute fakeIngressRoute + expected []*endpoint.Endpoint + }{ + { + title: "one rule.host one lb.hostname", + loadBalancer: fakeLoadBalancerService{ + hostnames: []string{"lb.com"}, // Kubernetes omits the trailing dot + }, + ingressRoute: fakeIngressRoute{ + host: "foo.bar", // Kubernetes requires removal of trailing dot + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "foo.bar", + Targets: endpoint.Targets{"lb.com"}, + }, + }, + }, + { + title: "one rule.host one lb.IP", + loadBalancer: fakeLoadBalancerService{ + ips: []string{"8.8.8.8"}, + }, + ingressRoute: fakeIngressRoute{ + host: "foo.bar", + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "foo.bar", + Targets: endpoint.Targets{"8.8.8.8"}, + }, + }, + }, + { + title: "one rule.host two lb.IP and two lb.Hostname", + loadBalancer: fakeLoadBalancerService{ + ips: []string{"8.8.8.8", "127.0.0.1"}, + hostnames: []string{"elb.com", "alb.com"}, + }, + ingressRoute: fakeIngressRoute{ + host: "foo.bar", + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "foo.bar", + Targets: endpoint.Targets{"8.8.8.8", "127.0.0.1"}, + }, + { + DNSName: "foo.bar", + Targets: endpoint.Targets{"elb.com", "alb.com"}, + }, + }, + }, + { + title: "no rule.host", + loadBalancer: fakeLoadBalancerService{ + ips: []string{"8.8.8.8", "127.0.0.1"}, + hostnames: []string{"elb.com", "alb.com"}, + }, + ingressRoute: fakeIngressRoute{}, + expected: []*endpoint.Endpoint{}, + }, + { + title: "one rule.host invalid ingressroute", + loadBalancer: fakeLoadBalancerService{ + ips: []string{"8.8.8.8", "127.0.0.1"}, + hostnames: []string{"elb.com", "alb.com"}, + }, + ingressRoute: fakeIngressRoute{ + host: "foo.bar", + invalid: true, + }, + expected: []*endpoint.Endpoint{}, + }, + { + title: "no targets", + loadBalancer: fakeLoadBalancerService{}, + ingressRoute: fakeIngressRoute{}, + expected: []*endpoint.Endpoint{}, + }, + } { + t.Run(ti.title, func(t *testing.T) { + if source, err := newTestIngressRouteSource(ti.loadBalancer); err != nil { + require.NoError(t, err) + } else if endpoints, err := source.endpointsFromIngressRoute(ti.ingressRoute.IngressRoute()); err != nil { + require.NoError(t, err) + } else { + validateEndpoints(t, endpoints, ti.expected) + } + }) + } +} + +func testIngressRouteEndpoints(t *testing.T) { + namespace := "testing" + for _, ti := range []struct { + title string + targetNamespace string + annotationFilter string + loadBalancer fakeLoadBalancerService + ingressRouteItems []fakeIngressRoute + expected []*endpoint.Endpoint + expectError bool + fqdnTemplate string + combineFQDNAndAnnotation bool + ignoreHostnameAnnotation bool + }{ + { + title: "no ingressroute", + targetNamespace: "", + }, + { + title: "two simple ingressroutes", + targetNamespace: "", + loadBalancer: fakeLoadBalancerService{ + ips: []string{"8.8.8.8"}, + hostnames: []string{"lb.com"}, + }, + ingressRouteItems: []fakeIngressRoute{ + { + name: "fake1", + namespace: namespace, + host: "example.org", + }, + { + name: "fake2", + namespace: namespace, + host: "new.org", + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "example.org", + Targets: endpoint.Targets{"8.8.8.8"}, + }, + { + DNSName: "example.org", + Targets: endpoint.Targets{"lb.com"}, + }, + { + DNSName: "new.org", + Targets: endpoint.Targets{"8.8.8.8"}, + }, + { + DNSName: "new.org", + Targets: endpoint.Targets{"lb.com"}, + }, + }, + }, + { + title: "two simple ingressroutes on different namespaces", + targetNamespace: "", + loadBalancer: fakeLoadBalancerService{ + ips: []string{"8.8.8.8"}, + hostnames: []string{"lb.com"}, + }, + ingressRouteItems: []fakeIngressRoute{ + { + name: "fake1", + namespace: "testing1", + host: "example.org", + }, + { + name: "fake2", + namespace: "testing2", + host: "new.org", + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "example.org", + Targets: endpoint.Targets{"8.8.8.8"}, + }, + { + DNSName: "example.org", + Targets: endpoint.Targets{"lb.com"}, + }, + { + DNSName: "new.org", + Targets: endpoint.Targets{"8.8.8.8"}, + }, + { + DNSName: "new.org", + Targets: endpoint.Targets{"lb.com"}, + }, + }, + }, + { + title: "two simple ingressroutes on different namespaces and a target namespace", + targetNamespace: "testing1", + loadBalancer: fakeLoadBalancerService{ + ips: []string{"8.8.8.8"}, + hostnames: []string{"lb.com"}, + }, + ingressRouteItems: []fakeIngressRoute{ + { + name: "fake1", + namespace: "testing1", + host: "example.org", + }, + { + name: "fake2", + namespace: "testing2", + host: "new.org", + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "example.org", + Targets: endpoint.Targets{"8.8.8.8"}, + }, + { + DNSName: "example.org", + Targets: endpoint.Targets{"lb.com"}, + }, + }, + }, + { + title: "valid matching annotation filter expression", + targetNamespace: "", + annotationFilter: "contour.heptio.com/ingress.class in (alb, contour)", + loadBalancer: fakeLoadBalancerService{ + ips: []string{"8.8.8.8"}, + }, + ingressRouteItems: []fakeIngressRoute{ + { + name: "fake1", + namespace: namespace, + annotations: map[string]string{ + "contour.heptio.com/ingress.class": "contour", + }, + host: "example.org", + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "example.org", + Targets: endpoint.Targets{"8.8.8.8"}, + }, + }, + }, + { + title: "valid non-matching annotation filter expression", + targetNamespace: "", + annotationFilter: "contour.heptio.com/ingress.class in (alb, contour)", + loadBalancer: fakeLoadBalancerService{ + ips: []string{"8.8.8.8"}, + }, + ingressRouteItems: []fakeIngressRoute{ + { + name: "fake1", + namespace: namespace, + annotations: map[string]string{ + "contour.heptio.com/ingress.class": "tectonic", + }, + host: "example.org", + }, + }, + expected: []*endpoint.Endpoint{}, + }, + { + title: "invalid annotation filter expression", + targetNamespace: "", + annotationFilter: "contour.heptio.com/ingress.name in (a b)", + loadBalancer: fakeLoadBalancerService{ + ips: []string{"8.8.8.8"}, + }, + ingressRouteItems: []fakeIngressRoute{ + { + name: "fake1", + namespace: namespace, + annotations: map[string]string{ + "contour.heptio.com/ingress.class": "alb", + }, + host: "example.org", + }, + }, + expected: []*endpoint.Endpoint{}, + expectError: true, + }, + { + title: "valid matching annotation filter label", + targetNamespace: "", + annotationFilter: "contour.heptio.com/ingress.class=contour", + loadBalancer: fakeLoadBalancerService{ + ips: []string{"8.8.8.8"}, + }, + ingressRouteItems: []fakeIngressRoute{ + { + name: "fake1", + namespace: namespace, + annotations: map[string]string{ + "contour.heptio.com/ingress.class": "contour", + }, + host: "example.org", + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "example.org", + Targets: endpoint.Targets{"8.8.8.8"}, + }, + }, + }, + { + title: "valid non-matching annotation filter label", + targetNamespace: "", + annotationFilter: "contour.heptio.com/ingress.class=contour", + loadBalancer: fakeLoadBalancerService{ + ips: []string{"8.8.8.8"}, + }, + ingressRouteItems: []fakeIngressRoute{ + { + name: "fake1", + namespace: namespace, + annotations: map[string]string{ + "contour.heptio.com/ingress.class": "alb", + }, + host: "example.org", + }, + }, + expected: []*endpoint.Endpoint{}, + }, + { + title: "our controller type is dns-controller", + targetNamespace: "", + loadBalancer: fakeLoadBalancerService{ + ips: []string{"8.8.8.8"}, + }, + ingressRouteItems: []fakeIngressRoute{ + { + name: "fake1", + namespace: namespace, + annotations: map[string]string{ + controllerAnnotationKey: controllerAnnotationValue, + }, + host: "example.org", + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "example.org", + Targets: endpoint.Targets{"8.8.8.8"}, + }, + }, + }, + { + title: "different controller types are ignored", + targetNamespace: "", + loadBalancer: fakeLoadBalancerService{ + ips: []string{"8.8.8.8"}, + }, + ingressRouteItems: []fakeIngressRoute{ + { + name: "fake1", + namespace: namespace, + annotations: map[string]string{ + controllerAnnotationKey: "some-other-tool", + }, + host: "example.org", + }, + }, + expected: []*endpoint.Endpoint{}, + }, + { + title: "template for ingressroute if host is missing", + targetNamespace: "", + loadBalancer: fakeLoadBalancerService{ + ips: []string{"8.8.8.8"}, + hostnames: []string{"elb.com"}, + }, + ingressRouteItems: []fakeIngressRoute{ + { + name: "fake1", + namespace: namespace, + annotations: map[string]string{ + controllerAnnotationKey: controllerAnnotationValue, + }, + host: "", + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "fake1.ext-dns.test.com", + Targets: endpoint.Targets{"8.8.8.8"}, + }, + { + DNSName: "fake1.ext-dns.test.com", + Targets: endpoint.Targets{"elb.com"}, + }, + }, + fqdnTemplate: "{{.Name}}.ext-dns.test.com", + }, + { + title: "another controller annotation skipped even with template", + targetNamespace: "", + loadBalancer: fakeLoadBalancerService{ + ips: []string{"8.8.8.8"}, + }, + ingressRouteItems: []fakeIngressRoute{ + { + name: "fake1", + namespace: namespace, + annotations: map[string]string{ + controllerAnnotationKey: "other-controller", + }, + host: "", + }, + }, + expected: []*endpoint.Endpoint{}, + fqdnTemplate: "{{.Name}}.ext-dns.test.com", + }, + { + title: "multiple FQDN template hostnames", + targetNamespace: "", + loadBalancer: fakeLoadBalancerService{ + ips: []string{"8.8.8.8"}, + }, + ingressRouteItems: []fakeIngressRoute{ + { + name: "fake1", + namespace: namespace, + annotations: map[string]string{}, + host: "", + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "fake1.ext-dns.test.com", + Targets: endpoint.Targets{"8.8.8.8"}, + RecordType: endpoint.RecordTypeA, + }, + { + DNSName: "fake1.ext-dna.test.com", + Targets: endpoint.Targets{"8.8.8.8"}, + RecordType: endpoint.RecordTypeA, + }, + }, + fqdnTemplate: "{{.Name}}.ext-dns.test.com, {{.Name}}.ext-dna.test.com", + }, + { + title: "multiple FQDN template hostnames", + targetNamespace: "", + loadBalancer: fakeLoadBalancerService{ + ips: []string{"8.8.8.8"}, + }, + ingressRouteItems: []fakeIngressRoute{ + { + name: "fake1", + namespace: namespace, + annotations: map[string]string{}, + host: "", + }, + { + name: "fake2", + namespace: namespace, + annotations: map[string]string{ + targetAnnotationKey: "ingressroute-target.com", + }, + host: "example.org", + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "fake1.ext-dns.test.com", + Targets: endpoint.Targets{"8.8.8.8"}, + RecordType: endpoint.RecordTypeA, + }, + { + DNSName: "fake1.ext-dna.test.com", + Targets: endpoint.Targets{"8.8.8.8"}, + RecordType: endpoint.RecordTypeA, + }, + { + DNSName: "example.org", + Targets: endpoint.Targets{"ingressroute-target.com"}, + RecordType: endpoint.RecordTypeCNAME, + }, + { + DNSName: "fake2.ext-dns.test.com", + Targets: endpoint.Targets{"ingressroute-target.com"}, + RecordType: endpoint.RecordTypeCNAME, + }, + { + DNSName: "fake2.ext-dna.test.com", + Targets: endpoint.Targets{"ingressroute-target.com"}, + RecordType: endpoint.RecordTypeCNAME, + }, + }, + fqdnTemplate: "{{.Name}}.ext-dns.test.com, {{.Name}}.ext-dna.test.com", + combineFQDNAndAnnotation: true, + }, + { + title: "ingressroute rules with annotation", + targetNamespace: "", + loadBalancer: fakeLoadBalancerService{ + ips: []string{"8.8.8.8"}, + }, + ingressRouteItems: []fakeIngressRoute{ + { + name: "fake1", + namespace: namespace, + annotations: map[string]string{ + targetAnnotationKey: "ingressroute-target.com", + }, + host: "example.org", + }, + { + name: "fake2", + namespace: namespace, + annotations: map[string]string{ + targetAnnotationKey: "ingressroute-target.com", + }, + host: "example2.org", + }, + { + name: "fake3", + namespace: namespace, + annotations: map[string]string{ + targetAnnotationKey: "1.2.3.4", + }, + host: "example3.org", + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "example.org", + Targets: endpoint.Targets{"ingressroute-target.com"}, + RecordType: endpoint.RecordTypeCNAME, + }, + { + DNSName: "example2.org", + Targets: endpoint.Targets{"ingressroute-target.com"}, + RecordType: endpoint.RecordTypeCNAME, + }, + { + DNSName: "example3.org", + Targets: endpoint.Targets{"1.2.3.4"}, + RecordType: endpoint.RecordTypeA, + }, + }, + }, + { + title: "ingressroute rules with hostname annotation", + targetNamespace: "", + loadBalancer: fakeLoadBalancerService{ + ips: []string{"1.2.3.4"}, + }, + ingressRouteItems: []fakeIngressRoute{ + { + name: "fake1", + namespace: namespace, + annotations: map[string]string{ + hostnameAnnotationKey: "dns-through-hostname.com", + }, + host: "example.org", + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "example.org", + Targets: endpoint.Targets{"1.2.3.4"}, + RecordType: endpoint.RecordTypeA, + }, + { + DNSName: "dns-through-hostname.com", + Targets: endpoint.Targets{"1.2.3.4"}, + RecordType: endpoint.RecordTypeA, + }, + }, + }, + { + title: "ingressroute rules with hostname annotation having multiple hostnames", + targetNamespace: "", + loadBalancer: fakeLoadBalancerService{ + ips: []string{"1.2.3.4"}, + }, + ingressRouteItems: []fakeIngressRoute{ + { + name: "fake1", + namespace: namespace, + annotations: map[string]string{ + hostnameAnnotationKey: "dns-through-hostname.com, another-dns-through-hostname.com", + }, + host: "example.org", + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "example.org", + Targets: endpoint.Targets{"1.2.3.4"}, + RecordType: endpoint.RecordTypeA, + }, + { + DNSName: "dns-through-hostname.com", + Targets: endpoint.Targets{"1.2.3.4"}, + RecordType: endpoint.RecordTypeA, + }, + { + DNSName: "another-dns-through-hostname.com", + Targets: endpoint.Targets{"1.2.3.4"}, + RecordType: endpoint.RecordTypeA, + }, + }, + }, + { + title: "ingressroute rules with hostname and target annotation", + targetNamespace: "", + loadBalancer: fakeLoadBalancerService{ + ips: []string{}, + }, + ingressRouteItems: []fakeIngressRoute{ + { + name: "fake1", + namespace: namespace, + annotations: map[string]string{ + hostnameAnnotationKey: "dns-through-hostname.com", + targetAnnotationKey: "ingressroute-target.com", + }, + host: "example.org", + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "example.org", + Targets: endpoint.Targets{"ingressroute-target.com"}, + RecordType: endpoint.RecordTypeCNAME, + }, + { + DNSName: "dns-through-hostname.com", + Targets: endpoint.Targets{"ingressroute-target.com"}, + RecordType: endpoint.RecordTypeCNAME, + }, + }, + }, + { + title: "ingressroute rules with annotation and custom TTL", + targetNamespace: "", + loadBalancer: fakeLoadBalancerService{ + ips: []string{"8.8.8.8"}, + }, + ingressRouteItems: []fakeIngressRoute{ + { + name: "fake1", + namespace: namespace, + annotations: map[string]string{ + targetAnnotationKey: "ingressroute-target.com", + ttlAnnotationKey: "6", + }, + host: "example.org", + }, + { + name: "fake2", + namespace: namespace, + annotations: map[string]string{ + targetAnnotationKey: "ingressroute-target.com", + ttlAnnotationKey: "1", + }, + host: "example2.org", + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "example.org", + Targets: endpoint.Targets{"ingressroute-target.com"}, + RecordTTL: endpoint.TTL(6), + }, + { + DNSName: "example2.org", + Targets: endpoint.Targets{"ingressroute-target.com"}, + RecordTTL: endpoint.TTL(1), + }, + }, + }, + { + title: "template for ingressroute with annotation", + targetNamespace: "", + loadBalancer: fakeLoadBalancerService{ + ips: []string{}, + hostnames: []string{}, + }, + ingressRouteItems: []fakeIngressRoute{ + { + name: "fake1", + namespace: namespace, + annotations: map[string]string{ + targetAnnotationKey: "ingressroute-target.com", + }, + host: "", + }, + { + name: "fake2", + namespace: namespace, + annotations: map[string]string{ + targetAnnotationKey: "ingressroute-target.com", + }, + host: "", + }, + { + name: "fake3", + namespace: namespace, + annotations: map[string]string{ + targetAnnotationKey: "1.2.3.4", + }, + host: "", + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "fake1.ext-dns.test.com", + Targets: endpoint.Targets{"ingressroute-target.com"}, + RecordType: endpoint.RecordTypeCNAME, + }, + { + DNSName: "fake2.ext-dns.test.com", + Targets: endpoint.Targets{"ingressroute-target.com"}, + RecordType: endpoint.RecordTypeCNAME, + }, + { + DNSName: "fake3.ext-dns.test.com", + Targets: endpoint.Targets{"1.2.3.4"}, + RecordType: endpoint.RecordTypeA, + }, + }, + fqdnTemplate: "{{.Name}}.ext-dns.test.com", + }, + { + title: "ingressroute with empty annotation", + targetNamespace: "", + loadBalancer: fakeLoadBalancerService{ + ips: []string{}, + hostnames: []string{}, + }, + ingressRouteItems: []fakeIngressRoute{ + { + name: "fake1", + namespace: namespace, + annotations: map[string]string{ + targetAnnotationKey: "", + }, + host: "", + }, + }, + expected: []*endpoint.Endpoint{}, + fqdnTemplate: "{{.Name}}.ext-dns.test.com", + }, + { + title: "ignore hostname annotations", + targetNamespace: "", + loadBalancer: fakeLoadBalancerService{ + ips: []string{"8.8.8.8"}, + hostnames: []string{"lb.com"}, + }, + ingressRouteItems: []fakeIngressRoute{ + { + name: "fake1", + namespace: namespace, + annotations: map[string]string{ + hostnameAnnotationKey: "ignore.me", + }, + host: "example.org", + }, + { + name: "fake2", + namespace: namespace, + annotations: map[string]string{ + hostnameAnnotationKey: "ignore.me.too", + }, + host: "new.org", + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "example.org", + Targets: endpoint.Targets{"8.8.8.8"}, + }, + { + DNSName: "example.org", + Targets: endpoint.Targets{"lb.com"}, + }, + { + DNSName: "new.org", + Targets: endpoint.Targets{"8.8.8.8"}, + }, + { + DNSName: "new.org", + Targets: endpoint.Targets{"lb.com"}, + }, + }, + ignoreHostnameAnnotation: true, + }, + } { + t.Run(ti.title, func(t *testing.T) { + ingressRoutes := make([]*contour.IngressRoute, 0) + for _, item := range ti.ingressRouteItems { + ingressRoutes = append(ingressRoutes, item.IngressRoute()) + } + + fakeKubernetesClient := fakeKube.NewSimpleClientset() + + lbService := ti.loadBalancer.Service() + _, err := fakeKubernetesClient.CoreV1().Services(lbService.Namespace).Create(lbService) + if err != nil { + require.NoError(t, err) + } + + fakeContourClient := fakeContour.NewSimpleClientset() + for _, ingressRoute := range ingressRoutes { + _, err := fakeContourClient.ContourV1beta1().IngressRoutes(ingressRoute.Namespace).Create(ingressRoute) + require.NoError(t, err) + } + + ingressRouteSource, err := NewContourIngressRouteSource( + fakeKubernetesClient, + fakeContourClient, + lbService.Namespace+"/"+lbService.Name, + ti.targetNamespace, + ti.annotationFilter, + ti.fqdnTemplate, + ti.combineFQDNAndAnnotation, + ti.ignoreHostnameAnnotation, + ) + require.NoError(t, err) + + res, err := ingressRouteSource.Endpoints() + if ti.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + validateEndpoints(t, res, ti.expected) + }) + } +} + +// ingressroute specific helper functions +func newTestIngressRouteSource(loadBalancer fakeLoadBalancerService) (*ingressRouteSource, error) { + fakeKubernetesClient := fakeKube.NewSimpleClientset() + fakeContourClient := fakeContour.NewSimpleClientset() + + lbService := loadBalancer.Service() + _, err := fakeKubernetesClient.CoreV1().Services(lbService.Namespace).Create(lbService) + if err != nil { + return nil, err + } + + src, err := NewContourIngressRouteSource( + fakeKubernetesClient, + fakeContourClient, + lbService.Namespace+"/"+lbService.Name, + "default", + "", + "{{.Name}}", + false, + false, + ) + if err != nil { + return nil, err + } + + irsrc, ok := src.(*ingressRouteSource) + if !ok { + return nil, errors.New("underlying source type was not ingressroute") + } + + return irsrc, nil +} + +type fakeLoadBalancerService struct { + ips []string + hostnames []string + namespace string + name string +} + +func (ig fakeLoadBalancerService) Service() *v1.Service { + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ig.namespace, + Name: ig.name, + }, + Status: v1.ServiceStatus{ + LoadBalancer: v1.LoadBalancerStatus{ + Ingress: []v1.LoadBalancerIngress{}, + }, + }, + } + + for _, ip := range ig.ips { + svc.Status.LoadBalancer.Ingress = append(svc.Status.LoadBalancer.Ingress, v1.LoadBalancerIngress{ + IP: ip, + }) + } + for _, hostname := range ig.hostnames { + svc.Status.LoadBalancer.Ingress = append(svc.Status.LoadBalancer.Ingress, v1.LoadBalancerIngress{ + Hostname: hostname, + }) + } + + return svc +} + +type fakeIngressRoute struct { + namespace string + name string + annotations map[string]string + + host string + invalid bool +} + +func (ir fakeIngressRoute) IngressRoute() *contour.IngressRoute { + var status string + if ir.invalid { + status = "invalid" + } else { + status = "valid" + } + + ingressRoute := &contour.IngressRoute{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ir.namespace, + Name: ir.name, + Annotations: ir.annotations, + }, + Spec: contour.IngressRouteSpec{ + VirtualHost: &contour.VirtualHost{ + Fqdn: ir.host, + }, + }, + Status: contour.Status{ + CurrentStatus: status, + }, + } + + return ingressRoute +} diff --git a/source/store.go b/source/store.go index 5491085e8..d48a63986 100644 --- a/source/store.go +++ b/source/store.go @@ -26,6 +26,7 @@ import ( "sync" cfclient "github.com/cloudfoundry-community/go-cfclient" + contour "github.com/heptio/contour/apis/generated/clientset/versioned" "github.com/linki/instrumented_http" log "github.com/sirupsen/logrus" istiocrd "istio.io/istio/pilot/pkg/config/kube/crd" @@ -57,6 +58,7 @@ type Config struct { CFAPIEndpoint string CFUsername string CFPassword string + ContourLoadBalancerService string } // ClientGenerator provides clients @@ -64,6 +66,7 @@ type ClientGenerator interface { KubeClient() (kubernetes.Interface, error) IstioClient() (istiomodel.ConfigStore, error) CloudFoundryClient(cfAPPEndpoint string, cfUsername string, cfPassword string) (*cfclient.Client, error) + ContourClient() (contour.Interface, error) } // SingletonClientGenerator stores provider clients and guarantees that only one instance of client @@ -75,9 +78,11 @@ type SingletonClientGenerator struct { kubeClient kubernetes.Interface istioClient istiomodel.ConfigStore cfClient *cfclient.Client + contourClient contour.Interface kubeOnce sync.Once istioOnce sync.Once cfOnce sync.Once + contourOnce sync.Once } // KubeClient generates a kube client if it was not created before @@ -122,6 +127,15 @@ func NewCFClient(cfAPIEndpoint string, cfUsername string, cfPassword string) (*c return client, nil } +// ContourClient generates a contour client if it was not created before +func (p *SingletonClientGenerator) ContourClient() (contour.Interface, error) { + var err error + p.contourOnce.Do(func() { + p.contourClient, err = NewContourClient(p.KubeConfig, p.KubeMaster, p.RequestTimeout) + }) + return p.contourClient, err +} + // ByNames returns multiple Sources given multiple names. func ByNames(p ClientGenerator, names []string, cfg *Config) ([]Source, error) { sources := []Source{} @@ -167,6 +181,16 @@ func BuildWithConfig(source string, p ClientGenerator, cfg *Config) (Source, err return nil, err } return NewCloudFoundrySource(cfClient) + case "contour-ingressroute": + kubernetesClient, err := p.KubeClient() + if err != nil { + return nil, err + } + contourClient, err := p.ContourClient() + if err != nil { + return nil, err + } + return NewContourIngressRouteSource(kubernetesClient, contourClient, cfg.ContourLoadBalancerService, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation) case "fake": return NewFakeSource(cfg.FQDNTemplate) case "connector": @@ -250,3 +274,39 @@ func NewIstioClient(kubeConfig string) (*istiocrd.Client, error) { return client, nil } + +// NewContourClient returns a new Contour client object. It takes a Config and +// uses KubeMaster and KubeConfig attributes to connect to the cluster. If +// KubeConfig isn't provided it defaults to using the recommended default. +func NewContourClient(kubeConfig, kubeMaster string, requestTimeout time.Duration) (*contour.Clientset, error) { + if kubeConfig == "" { + if _, err := os.Stat(clientcmd.RecommendedHomeFile); err == nil { + kubeConfig = clientcmd.RecommendedHomeFile + } + } + + config, err := clientcmd.BuildConfigFromFlags(kubeMaster, kubeConfig) + if err != nil { + return nil, err + } + + config.WrapTransport = func(rt http.RoundTripper) http.RoundTripper { + return instrumented_http.NewTransport(rt, &instrumented_http.Callbacks{ + PathProcessor: func(path string) string { + parts := strings.Split(path, "/") + return parts[len(parts)-1] + }, + }) + } + + config.Timeout = requestTimeout + + client, err := contour.NewForConfig(config) + if err != nil { + return nil, err + } + + log.Infof("Created Contour client %s", config.Host) + + return client, nil +} diff --git a/source/store_test.go b/source/store_test.go index 176ac9bbd..5377fce4e 100644 --- a/source/store_test.go +++ b/source/store_test.go @@ -20,13 +20,14 @@ import ( "errors" "testing" + cfclient "github.com/cloudfoundry-community/go-cfclient" + contour "github.com/heptio/contour/apis/generated/clientset/versioned" + fakeContour "github.com/heptio/contour/apis/generated/clientset/versioned/fake" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" istiomodel "istio.io/istio/pilot/pkg/model" "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/fake" - - cfclient "github.com/cloudfoundry-community/go-cfclient" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/suite" ) type MockClientGenerator struct { @@ -34,6 +35,7 @@ type MockClientGenerator struct { kubeClient kubernetes.Interface istioClient istiomodel.ConfigStore cloudFoundryClient *cfclient.Client + contourClient contour.Interface } func (m *MockClientGenerator) KubeClient() (kubernetes.Interface, error) { @@ -63,6 +65,15 @@ func (m *MockClientGenerator) CloudFoundryClient(cfAPIEndpoint string, cfUsernam return nil, args.Error(1) } +func (m *MockClientGenerator) ContourClient() (contour.Interface, error) { + args := m.Called() + if args.Error(1) == nil { + m.contourClient = args.Get(0).(contour.Interface) + return m.contourClient, nil + } + return nil, args.Error(1) +} + type ByNamesTestSuite struct { suite.Suite } @@ -71,10 +82,11 @@ func (suite *ByNamesTestSuite) TestAllInitialized() { mockClientGenerator := new(MockClientGenerator) mockClientGenerator.On("KubeClient").Return(fake.NewSimpleClientset(), nil) mockClientGenerator.On("IstioClient").Return(NewFakeConfigStore(), nil) + mockClientGenerator.On("ContourClient").Return(fakeContour.NewSimpleClientset(), nil) - sources, err := ByNames(mockClientGenerator, []string{"service", "ingress", "istio-gateway", "fake"}, minimalConfig) + sources, err := ByNames(mockClientGenerator, []string{"service", "ingress", "istio-gateway", "contour-ingressroute", "fake"}, minimalConfig) suite.NoError(err, "should not generate errors") - suite.Len(sources, 4, "should generate all four sources") + suite.Len(sources, 5, "should generate all five sources") } func (suite *ByNamesTestSuite) TestOnlyFake() { @@ -108,15 +120,22 @@ func (suite *ByNamesTestSuite) TestKubeClientFails() { _, err = ByNames(mockClientGenerator, []string{"istio-gateway"}, minimalConfig) suite.Error(err, "should return an error if kubernetes client cannot be created") + + _, err = ByNames(mockClientGenerator, []string{"contour-ingressroute"}, minimalConfig) + suite.Error(err, "should return an error if kubernetes client cannot be created") } func (suite *ByNamesTestSuite) TestIstioClientFails() { mockClientGenerator := new(MockClientGenerator) mockClientGenerator.On("KubeClient").Return(fake.NewSimpleClientset(), nil) mockClientGenerator.On("IstioClient").Return(nil, errors.New("foo")) + mockClientGenerator.On("ContourClient").Return(nil, errors.New("foo")) _, err := ByNames(mockClientGenerator, []string{"istio-gateway"}, minimalConfig) suite.Error(err, "should return an error if istio client cannot be created") + + _, err = ByNames(mockClientGenerator, []string{"contour-ingressroute"}, minimalConfig) + suite.Error(err, "should return an error if contour client cannot be created") } func TestByNames(t *testing.T) { @@ -125,4 +144,5 @@ func TestByNames(t *testing.T) { var minimalConfig = &Config{ IstioIngressGatewayServices: []string{"istio-system/istio-ingressgateway"}, + ContourLoadBalancerService: "heptio-contour/contour", }