diff --git a/Dockerfile b/Dockerfile index 3ea74c573..95dc63f1d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,7 +23,7 @@ RUN make build # final image FROM registry.opensource.zalan.do/stups/alpine:latest -MAINTAINER Team Teapot @ Zalando SE +LABEL maintainer="Team Teapot @ Zalando SE " COPY --from=builder /go/src/github.com/kubernetes-incubator/external-dns/build/external-dns /bin/external-dns diff --git a/Gopkg.lock b/Gopkg.lock index ece5c8e03..7ae69152a 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -2,7 +2,7 @@ [[projects]] - digest = "1:e94ea655a0038d2274be202f77a2ea0eb2d3f74dfee674fd5d1f541e81008039" + digest = "1:ae9d0182a5cf7dbb025a8fc5821234cc1f26ca342fc41d951a99f71b9adc1b87" name = "cloud.google.com/go" packages = [ "compute/metadata", @@ -12,7 +12,7 @@ revision = "3b1ae45394a234c385be014e9a488f2bb6eef821" [[projects]] - digest = "1:b341fb465b057e991b166d073b35a224f5a84228e5ef7e40b4da7a70c152e7ec" + digest = "1:fd38e3b8c27cab6561a7d2e8557205c3ca5c57cbb6d3a79e10f22e73e84fd776" name = "github.com/Azure/azure-sdk-for-go" packages = ["arm/dns"] pruneopts = "" @@ -20,7 +20,7 @@ version = "v10.0.4-beta" [[projects]] - digest = "1:767f5f5dd4fa8e4f7f206726361d29aa0f7622b0bb8294b73d071864368c0d6b" + digest = "1:f719ef698feb8da2923ebda9a8d553b977320b02182f3797e185202e588a94b1" name = "github.com/Azure/go-autorest" packages = [ "autorest", @@ -34,7 +34,7 @@ version = "v10.9.0" [[projects]] - digest = "1:283a95024c33e84b23f24b1b47e3157ff2df2517d786a2e17bb0e6e4955e94e4" + digest = "1:7dc69d1597e4773ec5f64e5c078d55f0f011bb05ec0435346d0649ad978a23fd" name = "github.com/alecthomas/kingpin" packages = ["."] pruneopts = "" @@ -43,7 +43,7 @@ [[projects]] branch = "master" - digest = "1:1399282ad03ac819f0e8a747c888407c5c98bb497d33821a7047c7bae667ede0" + digest = "1:a74730e052a45a3fab1d310fdef2ec17ae3d6af16228421e238320846f2aaec8" name = "github.com/alecthomas/template" packages = [ ".", @@ -61,7 +61,7 @@ revision = "2efee857e7cfd4f3d0138cc3cbb1b4966962b93a" [[projects]] - digest = "1:7b6c017b0290ccf1dd98c47a51e1db8b72b0863b6c7c52ddaa5a0d894aa3c2fc" + digest = "1:d2dc5d0ccc137594ea6fb3870964967b96b43daac19b8093930c7b02873fd5ca" name = "github.com/aliyun/alibaba-cloud-sdk-go" packages = [ "sdk", @@ -81,7 +81,7 @@ version = "1.27.7" [[projects]] - digest = "1:f04a72eefe1c7adec1dce30e099cec1e5fea8903a66e2db25bbbdfa66915428d" + digest = "1:1c82dd6a02941a3c4f23a32eca182717ab79691939e97d6b971466b780f06eea" name = "github.com/aws/aws-sdk-go" packages = [ "aws", @@ -121,14 +121,14 @@ [[projects]] branch = "master" - digest = "1:d20bdb6bf44087574af3139835946875bb098440426785282c741865b7bc66d3" + digest = "1:0c5485088ce274fac2e931c1b979f2619345097b39d91af3239977114adf0320" name = "github.com/beorn7/perks" packages = ["quantile"] pruneopts = "" revision = "4c0e84591b9aa9e6dcfdf3e020114cd81f89d5f9" [[projects]] - digest = "1:d9d9c71f9776ef8f15b5c0a20246d5303071294743863ac3f4dde056f8c7b40a" + digest = "1:85fd00554a6ed5b33687684b76635d532c74141508b5bce2843d85e8a3c9dc91" name = "github.com/cloudflare/cloudflare-go" packages = ["."] pruneopts = "" @@ -136,29 +136,22 @@ version = "v0.7.4" [[projects]] - digest = "1:31259dbcb4c073aace59b951f5b471b3d5dbc4051b4a9d7e000f4392e143977e" + digest = "1:eaeede87b418b97f9dee473f8940fd9b65ca5cdac0503350c7c8f8965ea3cf4d" name = "github.com/coreos/etcd" packages = [ - "client", - "pkg/pathutil", - "pkg/srv", + "auth/authpb", + "clientv3", + "etcdserver/api/v3rpc/rpctypes", + "etcdserver/etcdserverpb", + "mvcc/mvccpb", "pkg/types", - "version", ] pruneopts = "" revision = "1b3ac99e8a431b381e633802cc42fe70e663baf5" version = "v3.2.15" [[projects]] - digest = "1:3c3f68ebab415344aef64363d23471e953a4715645115604aaf57923ae904f5e" - name = "github.com/coreos/go-semver" - packages = ["semver"] - pruneopts = "" - revision = "8ab6407b697782a06568d4b7f1db25550ec2e4c6" - version = "v0.2.0" - -[[projects]] - digest = "1:0a39ec8bf5629610a4bc7873a92039ee509246da3cef1a0ea60f1ed7e5f9cea5" + digest = "1:56c130d885a4aacae1dd9c7b71cfe39912c7ebc1ff7d2b46083c8812996dc43b" name = "github.com/davecgh/go-spew" packages = ["spew"] pruneopts = "" @@ -167,7 +160,7 @@ [[projects]] branch = "master" - digest = "1:64ee6871ef691c663f910e29bc2f7c10c8c342b06665920f1138b6aa8b11cb5a" + digest = "1:dc166ce7345c060c2153561130d6d49ac580c804226ac675e368d533b36eb169" name = "github.com/denverdino/aliyungo" packages = [ "metadata", @@ -177,7 +170,7 @@ revision = "69560d9530f5265ba00ffad2520d7ef01c2cddd4" [[projects]] - digest = "1:2426da75f49e5b8507a6ed5d4c49b06b2ff795f4aec401c106b7db8fb2625cd7" + digest = "1:6098222470fe0172157ce9bbef5d2200df4edde17ee649c5d6e48330e4afa4c6" name = "github.com/dgrijalva/jwt-go" packages = ["."] pruneopts = "" @@ -185,7 +178,7 @@ version = "v3.2.0" [[projects]] - digest = "1:3da5806ef37ea163fee80ed179d40a5e013e671ccbe321a04c47c5aee3d5080a" + digest = "1:32d1941b093bb945de75b0276348494be318d34f3df39c4413d61e002c800bc6" name = "github.com/digitalocean/godo" packages = [ ".", @@ -196,7 +189,7 @@ version = "v1.1.1" [[projects]] - digest = "1:ca3b228bf258217cff2070f4045e53729886c66a27bf9cce30dcbf8a575ea86a" + digest = "1:5ffd39844bdd1259a6227d544f582c6686ce43c8c44399a46052fe3bd2bed93c" name = "github.com/dnsimple/dnsimple-go" packages = ["dnsimple"] pruneopts = "" @@ -204,7 +197,7 @@ version = "v0.14.0" [[projects]] - digest = "1:bfce2cc5b829073f93962e742275d45913948e22d182fbc5464104da1c5f2f89" + digest = "1:e17d18b233f506404061c27ac4a08624dad38dcd0d49f9cfdae67a7772a4fb8c" name = "github.com/exoscale/egoscale" packages = ["."] pruneopts = "" @@ -213,7 +206,7 @@ [[projects]] branch = "master" - digest = "1:bc12846e4bae094e01a33ef98cad0a1afa35da37090e5126513be6f747e074ab" + digest = "1:ae7fb2062735e966ab69d14d2a091f3778b0d676dc8d1f01d092bcb0fb8ed45b" name = "github.com/ffledgling/pdns-go" packages = ["."] pruneopts = "" @@ -228,7 +221,7 @@ version = "v1.0.0" [[projects]] - digest = "1:bbc763f3c703dc3c6a99a22c1318760099b52bc00a47a36dc4462e88eee7846b" + digest = "1:a00483fe4106b86fb1187a92b5cf6915c85f294ed4c129ccbe7cb1f1a06abd46" name = "github.com/go-ini/ini" packages = ["."] pruneopts = "" @@ -236,7 +229,7 @@ version = "v1.32.0" [[projects]] - digest = "1:cdeb6a9eb9f2356b2987c401d013d41e018b819ee1e8d5a1b32a5b714e53c392" + digest = "1:8e67153fc0a9fb0d6c9707e36cf80e217a012364307b222eb4ba6828f7e881e6" name = "github.com/go-resty/resty" packages = ["."] pruneopts = "" @@ -244,7 +237,7 @@ version = "v1.8.0" [[projects]] - digest = "1:d7b2f8af8341e15d0239dab17cb49fbf4f01029ecf2d3b5924aa53d95c5a452d" + digest = "1:54d5c6a784a9de9c836fc070d51d0689c3e99ee6d24dba8a36f0762039dae830" name = "github.com/gogo/googleapis" packages = ["google/rpc"] pruneopts = "" @@ -252,7 +245,7 @@ version = "v1.1.0" [[projects]] - digest = "1:673df1d02ca0c6f51458fe94bbb6fae0b05e54084a31db2288f1c4321255c2da" + digest = "1:6e73003ecd35f4487a5e88270d3ca0a81bc80dc88053ac7e4dcfec5fba30d918" name = "github.com/gogo/protobuf" packages = [ "gogoproto", @@ -276,11 +269,12 @@ source = "github.com/kubermatic/glog-logrus" [[projects]] - digest = "1:815d45503dceeca8ffecce0081d7edeae5e75b126107ef763d1c617154d72359" + digest = "1:3dd078fda7500c341bc26cfbc6c6a34614f295a2457149fc1045cab767cbcf18" name = "github.com/golang/protobuf" packages = [ "jsonpb", "proto", + "protoc-gen-go/descriptor", "ptypes", "ptypes/any", "ptypes/duration", @@ -315,7 +309,7 @@ revision = "44d81051d367757e1c7c6a5a86423ece9afcf63c" [[projects]] - digest = "1:1962b5d00f5285d08504697049627d45ad876912894528d31cdc1c05cdc853f6" + digest = "1:16b2837c8b3cf045fa2cdc82af0cf78b19582701394484ae76b2c3bc3c99ad73" name = "github.com/googleapis/gnostic" packages = [ "OpenAPIv2", @@ -328,7 +322,7 @@ [[projects]] branch = "master" - digest = "1:815036d12757902f85888f3cb0440c2e00220dd4177e4c2bb048e03259db077a" + digest = "1:54a44d48a24a104e078ef5f94d82f025a6be757e7c42b4370c621a3928d6ab7c" name = "github.com/gophercloud/gophercloud" packages = [ ".", @@ -362,7 +356,7 @@ [[projects]] branch = "master" - digest = "1:8c4d156acec272201ffc4d1bdb9302de1c48314e0451eb38c70150cf11bdb33a" + digest = "1:009a1928b8c096338b68b5822d838a72b4d8520715c1463614476359f3282ec8" name = "github.com/gregjones/httpcache" packages = [ ".", @@ -422,14 +416,14 @@ revision = "61dc5f9b0a655ebf43026f0d8a837ad1e28e4b96" [[projects]] - digest = "1:4f767a115bc8e08576f6d38ab73c376fc1b1cd3bb5041171c9e8668cc7739b52" + digest = "1:6f49eae0c1e5dab1dafafee34b207aeb7a42303105960944828c2079b92fc88e" name = "github.com/jmespath/go-jmespath" packages = ["."] pruneopts = "" revision = "0b12d6b5" [[projects]] - digest = "1:890dd7615573f096655600bbe7beb2f532a437f6d8ef237831894301fca31f23" + digest = "1:53ac4e911e12dde0ab68655e2006449d207a5a681f084974da2b06e5dbeaca72" name = "github.com/json-iterator/go" packages = ["."] pruneopts = "" @@ -437,14 +431,14 @@ version = "1.1.4" [[projects]] - digest = "1:def40684a573560241c8344da452fa3574dfc2c7da525903992a3790d2262625" + digest = "1:1c88ec29544b281964ed7a9a365b2802a523cd06c50cdee87eb3eec89cd864f4" name = "github.com/kubernetes/repo-infra" packages = ["verify/boilerplate/test"] pruneopts = "" revision = "c2f9667a4c29e70a39b0e89db2d4f0cab907dbee" [[projects]] - digest = "1:c3aa5f9d5119ca1cfdaa41a5084e3deceef0460eef3e6c71b58fa50e500f01a0" + digest = "1:7c23a751ce2f84663fa411acb87eae0da2d09c39a8e99b08bd8f65fae75d8928" name = "github.com/linki/instrumented_http" packages = ["."] pruneopts = "" @@ -452,7 +446,7 @@ version = "v0.2.0" [[projects]] - digest = "1:93d29291d0c37678592d77ee847031aec2ce1631f3ce4cf975b77216e8bd4a01" + digest = "1:1c41354ef11c9dbae2fe1ceee8369fcb2634977ba07e701e19ea53e8742c5420" name = "github.com/linode/linodego" packages = ["."] pruneopts = "" @@ -461,14 +455,14 @@ [[projects]] branch = "master" - digest = "1:49a8b01a6cd6558d504b65608214ca40a78000e1b343ed0da5c6a9ccd83d6d30" + digest = "1:63722a4b1e1717be7b98fc686e0b30d5e7f734b9e93d7dee86293b6deab7ea28" name = "github.com/matttproud/golang_protobuf_extensions" packages = ["pbutil"] pruneopts = "" revision = "c12348ce28de40eed0136aa2b644d0ee0650e56c" [[projects]] - digest = "1:f0bad0fece0fb73c6ea249c18d8e80ffbe86be0457715b04463068f04686cf39" + digest = "1:4c8d8358c45ba11ab7bb15df749d4df8664ff1582daead28bae58cf8cbe49890" name = "github.com/miekg/dns" packages = ["."] pruneopts = "" @@ -500,7 +494,7 @@ version = "v2.1" [[projects]] - digest = "1:7aef6d4ad1b4a613d66ed554010c552a249e9afabcb079f54528a298474549cc" + digest = "1:d8b5d0ecca348c835914a1ed8589f17a6a7f309befab7327b0470324531f7ac4" name = "github.com/nesv/go-dynect" packages = ["dynect"] pruneopts = "" @@ -508,7 +502,7 @@ version = "v0.6.0" [[projects]] - digest = "1:2062e45c462d0327f680340dce46fe11ae2d34bf802e15e397cb1d6c4d159b39" + digest = "1:70df8e71a953626770223d4982801fa73e7e99cbfcca068b95127f72af9b9edd" name = "github.com/oracle/oci-go-sdk" packages = [ "common", @@ -520,14 +514,14 @@ [[projects]] branch = "master" - digest = "1:b7be9a944fe102bf466420fa8a064534dd12547a0482f5b684d228425b559b56" + digest = "1:c24598ffeadd2762552269271b3b1510df2d83ee6696c1e543a0ff653af494bc" name = "github.com/petar/GoLLRB" packages = ["llrb"] pruneopts = "" revision = "53be0d36a84c2a886ca057d34b6aa4468df9ccb4" [[projects]] - digest = "1:6db21ad64a13fe79220e47fcc895e13b8da923676a3a024f98210fca57a10d9a" + digest = "1:b46305723171710475f2dd37547edd57b67b9de9f2a6267cafdd98331fd6897f" name = "github.com/peterbourgon/diskv" packages = ["."] pruneopts = "" @@ -550,7 +544,7 @@ version = "v1.0.0" [[projects]] - digest = "1:7e88bda1bec34ddf3c8aded1326c652793069a673b0f751484953e7d65a2386c" + digest = "1:2f69dc6b2685b31a1a410ef697410aa8a669704fb201d45dbd8c1911728afa75" name = "github.com/prometheus/client_golang" packages = [ "prometheus", @@ -562,7 +556,7 @@ [[projects]] branch = "master" - digest = "1:83bf37d060fca77e959fe5ceee81e58bbd1b01836f4addc70043a948e9912547" + digest = "1:60aca47f4eeeb972f1b9da7e7db51dee15ff6c59f7b401c1588b8e6771ba15ef" name = "github.com/prometheus/client_model" packages = ["go"] pruneopts = "" @@ -570,7 +564,7 @@ [[projects]] branch = "master" - digest = "1:7221d79e41a24b2245d06f331d0825b479a9acd0bd05a8353806c7bf38395795" + digest = "1:e3aa5178be4fc4ae8cdb37d11c02f7490c00450a9f419e6aa84d02d3b47e90d2" name = "github.com/prometheus/common" packages = [ "expfmt", @@ -581,7 +575,7 @@ revision = "2e54d0b93cba2fd133edc32211dcc32c06ef72ca" [[projects]] - digest = "1:91345f4cce04248cf4998c4f70a82579c1468101767636acf5af2e1556904933" + digest = "1:a6a85fc81f2a06ccac3d45005523afbeee45138d781d4f3cb7ad9889d5c65aab" name = "github.com/prometheus/procfs" packages = [ ".", @@ -599,7 +593,7 @@ version = "v1.2.0" [[projects]] - digest = "1:75e2c10fd48881dc9400b7b70281270923e01c44f1f5cb4bbc5ba8cac8ca3026" + digest = "1:3ac248add5bb40a3c631c5334adcd09aa72d15af2768a5bc0274084ea7b2e5ba" name = "github.com/sirupsen/logrus" packages = ["."] pruneopts = "" @@ -607,7 +601,7 @@ version = "v1.0.3" [[projects]] - digest = "1:39c598f67d5d68846c05832bb351e897091edcbee4689c57d3697f68f25f928d" + digest = "1:a1403cc8a94b8d7956ee5e9694badef0e7b051af289caad1cf668331e3ffa4f6" name = "github.com/spf13/cobra" packages = ["."] pruneopts = "" @@ -623,7 +617,7 @@ version = "v1.0.2" [[projects]] - digest = "1:ba8fed52de60135b7efd5d832b997fb5b10fa09f227fa385174faa69f4219e4e" + digest = "1:306417ea2f31ea733df356a2b895de63776b6a5107085b33458e5cd6eb1d584d" name = "github.com/stretchr/objx" packages = ["."] pruneopts = "" @@ -631,7 +625,7 @@ version = "v0.1" [[projects]] - digest = "1:a70d585d45f695f2e8e6782569bdf181419667a35e6035ceb086706b495aa21a" + digest = "1:a30066593578732a356dc7e5d7f78d69184ca65aeeff5939241a3ab10559bb06" name = "github.com/stretchr/testify" packages = [ "assert", @@ -652,14 +646,7 @@ revision = "ac974c61c2f990f4115b119354b5e0b47550e888" [[projects]] - digest = "1:f98e0b7c7bd110a49d8bb56c9eefcef4f547f5d789025d3bfe9bd6b83125221b" - name = "github.com/ugorji/go" - packages = ["codec"] - pruneopts = "" - revision = "ded73eae5db7e7a0ef6f55aace87a2873c5d2b74" - -[[projects]] - digest = "1:5e30725e7522642910b34208061b21bb0cd77b8ce115c3133a1431c52054e004" + digest = "1:74f86c458e82e1c4efbab95233e0cf51b7cc02dc03193be9f62cd81224e10401" name = "go.uber.org/atomic" packages = ["."] pruneopts = "" @@ -675,7 +662,7 @@ version = "v1.1.0" [[projects]] - digest = "1:2fabb14a874994210af33633091bd0eb070b50aa527767abaac1b5483db03d75" + digest = "1:246f378f80fba6fcf0f191c486b6613265abd2bc0f2fa55a36b928c67352021e" name = "go.uber.org/zap" packages = [ ".", @@ -692,7 +679,7 @@ [[projects]] branch = "master" - digest = "1:16db3d6f4f8bbe4b7b42cb8808e68457fea4bd7aea410b77c8c9a6dc26253a60" + digest = "1:b2d8b39397ca07929a3de3a3fd2b6ca4c8d48e9cadaa7cf2b083e27fd9e78107" name = "golang.org/x/crypto" packages = [ "ed25519", @@ -703,7 +690,7 @@ revision = "0709b304e793a5edb4a2c0145f281ecdc20838a4" [[projects]] - digest = "1:02feed0dbc44ce5bef5670a6e5ac9c2c4e3b879575a9d074199b487af1b7c4f9" + digest = "1:782723d6fc27d202f1943219d68d58b3f6bcab6212c85294b1ddd8b586b1d356" name = "golang.org/x/net" packages = [ "bpf", @@ -725,7 +712,7 @@ revision = "161cd47e91fd58ac17490ef4d742dc98bb4cf60e" [[projects]] - digest = "1:2fef2e19e90f29efd775d58d66b5e100139fedbe24cf749f1c085c0a5aee86d3" + digest = "1:dad5a319c4710358be1f2bf68f9fb7f90a71d7c641221b79801d5667b28f19e3" name = "golang.org/x/oauth2" packages = [ ".", @@ -738,7 +725,7 @@ revision = "3c3a985cb79f52a3190fbc056984415ca6763d01" [[projects]] - digest = "1:d4315e749759007a597c9ad09eef29112bea98030da19ed29c33959ad6744130" + digest = "1:39d88a855976e160babdd254802e1c2ae75ed380328c39742b57928852da6207" name = "golang.org/x/sys" packages = [ "unix", @@ -748,7 +735,7 @@ revision = "13d03a9a82fba647c21a0ef8fba44a795d0f0835" [[projects]] - digest = "1:af9bfca4298ef7502c52b1459df274eed401a4f5498b900e9a92d28d3d87ac5a" + digest = "1:5acd3512b047305d49e8763eef7ba423901e85d5dd2fd1e71778a0ea8de10bd4" name = "golang.org/x/text" packages = [ "collate", @@ -779,7 +766,7 @@ revision = "fbb02b2291d28baffd63558aa44b4b56f178d650" [[projects]] - digest = "1:ab84306e7e74b9f01b9f1480d46cca9325f8c512567a0e7b8888d04ff627a5ba" + digest = "1:2ad38d79865e33dde6157b7048debd6e7d47e0709df7b5e11bb888168e316908" name = "google.golang.org/api" packages = [ "dns/v1", @@ -791,7 +778,7 @@ revision = "a0ff90edab763c86aa88f2b1eb4f3301b82f6336" [[projects]] - digest = "1:0b45fac4876cbd496ed7b95406b05c8c1eba559b43c82f2dda1b0e1bbe6cd1b6" + digest = "1:41e2b7e287117f6136f75286d48072ecf781ba54823ffeb2098e152e7dc45ef6" name = "google.golang.org/appengine" packages = [ ".", @@ -810,14 +797,17 @@ [[projects]] branch = "master" - digest = "1:7040eaf95eb09f6f69e1415074049a9a66236d59d8767f2d17b759b916f79fb1" + digest = "1:e43f1cb3f488a0c2be85939c2a594636f60b442a12a196c778bd2d6c9aca3df7" name = "google.golang.org/genproto" - packages = ["googleapis/rpc/status"] + packages = [ + "googleapis/api/annotations", + "googleapis/rpc/status", + ] pruneopts = "" revision = "11092d34479b07829b72e10713b159248caf5dad" [[projects]] - digest = "1:cb1330030248de97a11d9f9664f3944fce0df947e5ed94dbbd9cb6e77068bd46" + digest = "1:ca75b3775a5d4e5d1fb48f57ef0865b4aaa8b3f00e6b52be68db991c4594e0a7" name = "google.golang.org/grpc" packages = [ ".", @@ -830,6 +820,7 @@ "encoding", "encoding/proto", "grpclog", + "health/grpc_health_v1", "internal", "internal/backoff", "internal/channelz", @@ -869,7 +860,7 @@ [[projects]] branch = "release-1.0" - digest = "1:159d72863d2fdc7f8a7bf5178554bad51c45b003ba27021c3481f84b3cc8e155" + digest = "1:bc43af6616d8ca12a7b8e806874387f0f1e181c08f547e9cd77f6cbac8cefd86" name = "istio.io/api" packages = [ "authentication/v1alpha1", @@ -883,7 +874,7 @@ revision = "76349c53b87f03f1e610b3aa3843dba3c38138d7" [[projects]] - digest = "1:2e63e5a0a6abb75c20ea575ee82a72c117a811ab8472ed34a48a09ba25a0a7d7" + digest = "1:7eb25280e1f610470bb0c43ab6c91573cfc78672a58542106b9b71705581429a" name = "istio.io/istio" packages = [ "pilot/pkg/config/kube/crd", @@ -899,7 +890,7 @@ version = "1.0.1" [[projects]] - digest = "1:d42e6aef075bfc20da9c3991adfd8b09e3e158ac619028e15271677b705be5f0" + digest = "1:f420c8548c93242d8e5dcfa5b34e0243883b4e660f65076e869daafac877144d" name = "k8s.io/api" packages = [ "admissionregistration/v1alpha1", @@ -937,7 +928,7 @@ version = "kubernetes-1.11.0" [[projects]] - digest = "1:a14992570e0e2b0291594f505d2b2ed1a6ba4482d4166ace9714c2ba8cbfe252" + digest = "1:66d1421ecff35bc48ee0b11a3f891f3af6f775ed6bb1d3e0deeaba221bf42490" name = "k8s.io/apiextensions-apiserver" packages = [ "pkg/apis/apiextensions", @@ -951,7 +942,7 @@ version = "kubernetes-1.10.4" [[projects]] - digest = "1:a855f74be59f83ed0950a9a2b70d8c8af01fb5782d060c7dec67ae39033f30dc" + digest = "1:b6b2fb7b4da1ac973b64534ace2299a02504f16bc7820cb48edb8ca4077183e1" name = "k8s.io/apimachinery" packages = [ "pkg/api/errors", @@ -1001,7 +992,7 @@ version = "kubernetes-1.11.0" [[projects]] - digest = "1:3c4611c2b28fdc62391698bba7f212050f0f9ed75f3648f37ec3bcf8a83bf96d" + digest = "1:d04779a8de7d5465e0463bd986506348de5e89677c74777f695d3145a7a8d15e" name = "k8s.io/client-go" packages = [ "discovery", @@ -1107,7 +1098,7 @@ [[projects]] branch = "master" - digest = "1:d93d8bcb5f04d6b59eafdb9fa1a80f187d2542611670bfabc0ea8e031ab874a2" + digest = "1:526095379da1098c3f191a0008cc59c9bf9927492e63da7689e5de424219c162" name = "k8s.io/kube-openapi" packages = ["pkg/util/proto"] pruneopts = "" @@ -1134,7 +1125,7 @@ "github.com/aws/aws-sdk-go/service/route53", "github.com/aws/aws-sdk-go/service/servicediscovery", "github.com/cloudflare/cloudflare-go", - "github.com/coreos/etcd/client", + "github.com/coreos/etcd/clientv3", "github.com/denverdino/aliyungo/metadata", "github.com/digitalocean/godo", "github.com/digitalocean/godo/context", @@ -1173,6 +1164,7 @@ "istio.io/istio/pilot/pkg/model", "k8s.io/api/core/v1", "k8s.io/api/extensions/v1beta1", + "k8s.io/apimachinery/pkg/api/errors", "k8s.io/apimachinery/pkg/apis/meta/v1", "k8s.io/apimachinery/pkg/labels", "k8s.io/apimachinery/pkg/runtime", diff --git a/Gopkg.toml b/Gopkg.toml index b7569514c..328d11f42 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -24,10 +24,6 @@ ignored = ["github.com/kubernetes/repo-infra/kazel"] name = "github.com/cloudflare/cloudflare-go" version = "0.7.3" -[[constraint]] - name = "github.com/coreos/etcd" - version = "~3.2.15" - [[constraint]] name = "github.com/digitalocean/godo" version = "~1.1.0" diff --git a/controller/controller_test.go b/controller/controller_test.go index 49e1446e9..909c33a78 100644 --- a/controller/controller_test.go +++ b/controller/controller_test.go @@ -90,12 +90,14 @@ func TestRunOnce(t *testing.T) { source := new(testutils.MockSource) source.On("Endpoints").Return([]*endpoint.Endpoint{ { - DNSName: "create-record", - Targets: endpoint.Targets{"1.2.3.4"}, + DNSName: "create-record", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"1.2.3.4"}, }, { - DNSName: "update-record", - Targets: endpoint.Targets{"8.8.4.4"}, + DNSName: "update-record", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.4.4"}, }, }, nil) @@ -103,26 +105,28 @@ func TestRunOnce(t *testing.T) { provider := newMockProvider( []*endpoint.Endpoint{ { - DNSName: "update-record", - Targets: endpoint.Targets{"8.8.8.8"}, + DNSName: "update-record", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, }, { - DNSName: "delete-record", - Targets: endpoint.Targets{"4.3.2.1"}, + DNSName: "delete-record", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"4.3.2.1"}, }, }, &plan.Changes{ Create: []*endpoint.Endpoint{ - {DNSName: "create-record", Targets: endpoint.Targets{"1.2.3.4"}}, + {DNSName: "create-record", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, }, UpdateNew: []*endpoint.Endpoint{ - {DNSName: "update-record", Targets: endpoint.Targets{"8.8.4.4"}}, + {DNSName: "update-record", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"8.8.4.4"}}, }, UpdateOld: []*endpoint.Endpoint{ - {DNSName: "update-record", Targets: endpoint.Targets{"8.8.8.8"}}, + {DNSName: "update-record", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"8.8.8.8"}}, }, Delete: []*endpoint.Endpoint{ - {DNSName: "delete-record", Targets: endpoint.Targets{"4.3.2.1"}}, + {DNSName: "delete-record", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"4.3.2.1"}}, }, }, ) diff --git a/docs/faq.md b/docs/faq.md index dcf6873fb..d5a1666ab 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -69,6 +69,7 @@ Regarding Ingress, we'll support: * Google's Ingress Controller on GKE that integrates with their Layer 7 load balancers (GLBC) * nginx-ingress-controller v0.9.x with a fronting Service * Zalando's [AWS Ingress controller](https://github.com/zalando-incubator/kube-ingress-aws-controller), based on AWS ALBs and [Skipper](https://github.com/zalando/skipper) +* [Traefik](https://github.com/containous/traefik) 1.7 and above, when [`kubernetes.ingressEndpoint`](https://docs.traefik.io/v1.7/configuration/backends/kubernetes/#ingressendpoint) is configured (`kubernetes.ingressEndpoint.useDefaultPublishedService` in the [Helm chart](https://github.com/helm/charts/tree/master/stable/traefik#configuration)) ### Are other Ingress Controllers supported? diff --git a/docs/tutorials/alibabacloud.md b/docs/tutorials/alibabacloud.md index b9b5a5789..2fc509579 100644 --- a/docs/tutorials/alibabacloud.md +++ b/docs/tutorials/alibabacloud.md @@ -286,7 +286,7 @@ spec: After roughly two minutes check that a corresponding DNS record for your service was created. ```console -$ aliyun aliyun alidns DescribeDomainRecords --DomainName=external-dns-test.com +$ aliyun alidns DescribeDomainRecords --DomainName=external-dns-test.com { "PageNumber": 1, "TotalCount": 1, diff --git a/docs/tutorials/aws.md b/docs/tutorials/aws.md index dc9be8ad9..5ab4050de 100644 --- a/docs/tutorials/aws.md +++ b/docs/tutorials/aws.md @@ -171,6 +171,13 @@ This list is not the full list, but a few arguments that where chosen. `aws-zone-type` allows filtering for private and public zones +## Annotations + +Annotations which are specific to AWS. + +### alias + +`external-dns.alpha.kubernetes.io/alias` if set to `true` on an ingress, it will create an ALIAS record when the target is an ALIAS as well. ## Verify ExternalDNS works (Ingress example) diff --git a/docs/tutorials/azure.md b/docs/tutorials/azure.md index 0df336cf2..7d1a59515 100644 --- a/docs/tutorials/azure.md +++ b/docs/tutorials/azure.md @@ -3,7 +3,7 @@ This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster on Azure. -Make sure to use **>=0.4.2** version of ExternalDNS for this tutorial. +Make sure to use **>=0.5.7** version of ExternalDNS for this tutorial. This tutorial uses [Azure CLI 2.0](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli) for all Azure commands and assumes that the Kubernetes cluster was created via Azure Container Services and `kubectl` commands @@ -38,22 +38,57 @@ Please consult your registrar's documentation on how to do that. 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. -### Azure Container Services -When your Kubernetes cluster is created by ACS, a file named `/etc/kubernetes/azure.json` is created to store -the Azure credentials for API access. Kubernetes uses this file for the Azure cloud provider. +### 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`. -For ExternalDNS to access the Azure API, it also needs access to this file. However, we will be deploying ExternalDNS inside of -the Kubernetes cluster so we will need to use a Kubernetes secret. +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: + +```json +{ + "tenantId": "01234abc-de56-ff78-abc1-234567890def", + "subscriptionId": "01234abc-de56-ff78-abc1-234567890def", + "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 `subscriptionId` by running `az account show --query "id"` or by selecting Subscriptions in the Azure Portal. To create the secret: ``` -$ kubectl create secret generic azure-config-file --from-file=/etc/kubernetes/azure.json +$ kubectl create secret generic azure-config-file --from-file=/local/path/to/azure.json ``` -### Azure Kubernetes Services (aka AKS) -When your cluster is created, unlike ACS there are no Azure credentials stored and you must create an azure.json object manually like with other hosting providers. In order to create the azure.json you must first create an Azure AD service principal in the Azure AD tenant linked to your Azure subscription that is hosting your DNS zone. - #### Create 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. @@ -89,27 +124,20 @@ A Service Principal with a minimum access level of contribute to the resource gr "password": "password", <-- aadClientSecret value "tenant": "AzureAD Tenant Id" <-- tenantId value } -... +``` -``` -### Other hosting providers -If the Kubernetes cluster is not hosted by Azure Container Services and you still want to use Azure DNS, you need to create the secret manually. The secret should contain an object named azure.json with content similar to this: -``` +#### 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. + +The contents of `azure.json` should be similar to this: + +```json { - "tenantId": "AzureAD tenant Id", - "subscriptionId": "Id", - "aadClientId": "Service Principal AppId", - "aadClientSecret": "Service Principal Password", + "tenantId": "01234abc-de56-ff78-abc1-234567890def", + "subscriptionId": "01234abc-de56-ff78-abc1-234567890def", "resourceGroup": "MyDnsResourceGroup", -} -``` -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. The contents of `azure.json` should be similar to this: -``` -{ - "tenantId": "AzureAD tenant Id", - "subscriptionId": "Id", - "resourceGroup": "MyDnsResourceGroup", - "useManagedIdentityExtension": true, + "useManagedIdentityExtension": true } ``` @@ -170,7 +198,7 @@ spec: secretName: azure-config-file ``` -### Manifest (for clusters with RBAC enabled) +### Manifest (for clusters with RBAC enabled, cluster access) ```yaml apiVersion: v1 kind: ServiceAccount @@ -240,6 +268,76 @@ spec: secretName: azure-config-file ``` +### Manifest (for clusters with RBAC enabled, namespace access) +This configuration is the same as above, except it only requires privileges for the current namespace, not for the whole cluster. +However, access to [nodes](https://kubernetes.io/docs/concepts/architecture/nodes/) requires cluster access, so when using this manifest, +services with type `NodePort` will be skipped! + +```yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: external-dns +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: Role +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"] +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: RoleBinding +metadata: + name: external-dns +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: external-dns +subjects: +- kind: ServiceAccount + name: external-dns +--- +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: external-dns +spec: + strategy: + type: Recreate + 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=service + - --source=ingress + - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. + - --provider=azure + - --azure-resource-group=externaldns # (optional) use the DNS zones from the tutorial's resource group + volumeMounts: + - name: azure-config-file + mountPath: /etc/kubernetes + readOnly: true + volumes: + - name: azure-config-file + secret: + secretName: azure-config-file +``` + Create the deployment for ExternalDNS: ``` diff --git a/docs/tutorials/oracle.md b/docs/tutorials/oracle.md index adfab29c2..9fb6de42a 100644 --- a/docs/tutorials/oracle.md +++ b/docs/tutorials/oracle.md @@ -22,6 +22,7 @@ auth: region: us-phoenix-1 tenancy: ocid1.tenancy.oc1... user: ocid1.user.oc1... + key: | -----BEGIN RSA PRIVATE KEY----- -----END RSA PRIVATE KEY----- fingerprint: af:81:71:8e... diff --git a/docs/tutorials/pdns.md b/docs/tutorials/pdns.md index 33b43f3bb..b04ccfee0 100644 --- a/docs/tutorials/pdns.md +++ b/docs/tutorials/pdns.md @@ -15,8 +15,7 @@ anyway. The PDNS provider currently does not support: -1. Dry running a configuration is not supported. -2. The `--domain-filter` flag is not supported. +* Dry running a configuration is not supported ## Deployment @@ -47,10 +46,18 @@ spec: - --pdns-server={{ pdns-api-url }} - --pdns-api-key={{ pdns-http-api-key }} - --txt-owner-id={{ owner-id-for-this-external-dns }} + - --domain-filter=external-dns-test.my-org.com # will make ExternalDNS see only the zones matching provided domain; omit to process all available zones in PowerDNS - --log-level=debug - --interval=30s ``` +#### Domain Filter (--domain-filter) +When the domain-filter argument is specified, external-dns will automatically create DNS records based on host names specified in ingress objects and services with the external-dns annotation that match the domain-filter argument in the external-dns deployment manifest. + +eg. ```--domain-filter=example.org``` will allow for zone `example.org` and any zones in PowerDNS that ends in `.example.org`, including `an.example.org`, ie. the subdomains of example.org. + +eg. ```--domain-filter=.example.org``` will allow *only* zones that end in `.example.org`, ie. the subdomains of example.org but not the `example.org` zone itself. + ## RBAC If your cluster is RBAC enabled, you also need to setup the following, before you can run external-dns: diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index d518f99d9..d458aea8a 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -288,7 +288,7 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("rfc2136-tsig-axfr", "When using the RFC2136 provider, specify the TSIG (base64) value to attached to DNS messages (required when --rfc2136-insecure=false)").BoolVar(&cfg.RFC2136TAXFR) // Flags related to policies - app.Flag("policy", "Modify how DNS records are sychronized between sources and providers (default: sync, options: sync, upsert-only)").Default(defaultConfig.Policy).EnumVar(&cfg.Policy, "sync", "upsert-only") + app.Flag("policy", "Modify how DNS records are synchronized between sources and providers (default: sync, options: sync, upsert-only)").Default(defaultConfig.Policy).EnumVar(&cfg.Policy, "sync", "upsert-only") // Flags related to the registry app.Flag("registry", "The registry implementation to use to keep track of DNS record ownership (default: txt, options: txt, noop, aws-sd)").Default(defaultConfig.Registry).EnumVar(&cfg.Registry, "txt", "noop", "aws-sd") diff --git a/plan/plan.go b/plan/plan.go index 93c5494e3..21bf5b677 100644 --- a/plan/plan.go +++ b/plan/plan.go @@ -135,10 +135,10 @@ func (t planTable) getDeletes() (deleteList []*endpoint.Endpoint) { func (p *Plan) Calculate() *Plan { t := newPlanTable() - for _, current := range p.Current { + for _, current := range filterRecordsForPlan(p.Current) { t.addCurrent(current) } - for _, desired := range p.Desired { + for _, desired := range filterRecordsForPlan(p.Desired) { t.addCandidate(desired) } @@ -180,6 +180,30 @@ func shouldUpdateTTL(desired, current *endpoint.Endpoint) bool { return desired.RecordTTL != current.RecordTTL } +// filterRecordsForPlan removes records that are not relevant to the planner. +// Currently this just removes TXT records to prevent them from being +// deleted erroneously by the planner (only the TXT registry should do this.) +// +// Per RFC 1034, CNAME records conflict with all other records - it is the +// only record with this property. The behavior of the planner may need to be +// made more sophisticated to codify this. +func filterRecordsForPlan(records []*endpoint.Endpoint) []*endpoint.Endpoint { + filtered := []*endpoint.Endpoint{} + + for _, record := range records { + // Explicitly specify which records we want to use for planning. + // TODO: Add AAAA records as well when they are supported. + switch record.RecordType { + case endpoint.RecordTypeA, endpoint.RecordTypeCNAME: + filtered = append(filtered, record) + default: + continue + } + } + + return filtered +} + // sanitizeDNSName checks if the DNS name is correct // for now it only removes space and lower case func sanitizeDNSName(dnsName string) string { diff --git a/plan/plan_test.go b/plan/plan_test.go index d29d1c0da..e36b343a6 100644 --- a/plan/plan_test.go +++ b/plan/plan_test.go @@ -29,6 +29,7 @@ type PlanTestSuite struct { suite.Suite fooV1Cname *endpoint.Endpoint fooV2Cname *endpoint.Endpoint + fooV2TXT *endpoint.Endpoint fooV2CnameNoLabel *endpoint.Endpoint fooV3CnameSameResource *endpoint.Endpoint fooA5 *endpoint.Endpoint @@ -65,6 +66,10 @@ func (suite *PlanTestSuite) SetupTest() { endpoint.ResourceLabelKey: "ingress/default/foo-v2", }, } + suite.fooV2TXT = &endpoint.Endpoint{ + DNSName: "foo", + RecordType: "TXT", + } suite.fooV2CnameNoLabel = &endpoint.Endpoint{ DNSName: "foo", Targets: endpoint.Targets{"v2"}, @@ -262,6 +267,27 @@ func (suite *PlanTestSuite) TestDifferentTypes() { validateEntries(suite.T(), changes.Delete, expectedDelete) } +func (suite *PlanTestSuite) TestIgnoreTXT() { + current := []*endpoint.Endpoint{suite.fooV2TXT} + desired := []*endpoint.Endpoint{suite.fooV2Cname} + expectedCreate := []*endpoint.Endpoint{suite.fooV2Cname} + expectedUpdateOld := []*endpoint.Endpoint{} + expectedUpdateNew := []*endpoint.Endpoint{} + expectedDelete := []*endpoint.Endpoint{} + + p := &Plan{ + Policies: []Policy{&SyncPolicy{}}, + Current: current, + Desired: desired, + } + + changes := p.Calculate().Changes + validateEntries(suite.T(), changes.Create, expectedCreate) + validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew) + validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld) + validateEntries(suite.T(), changes.Delete, expectedDelete) +} + func (suite *PlanTestSuite) TestRemoveEndpoint() { current := []*endpoint.Endpoint{suite.fooV1Cname, suite.bar192A} desired := []*endpoint.Endpoint{suite.fooV1Cname} diff --git a/provider/alibaba_cloud.go b/provider/alibaba_cloud.go index 7100bf5bb..79663577d 100644 --- a/provider/alibaba_cloud.go +++ b/provider/alibaba_cloud.go @@ -684,18 +684,24 @@ func (p *AlibabaCloudProvider) splitDNSName(endpoint *endpoint.Endpoint) (rr str } if !found { - idx := strings.Index(name, ".") - if idx >= 0 { - rr = name[0:idx] - domain = name[idx+1:] - } else { + parts := strings.Split(name, ".") + if len(parts) < 2 { rr = name domain = "" + } else { + domain = parts[len(parts)-2] + "." + parts[len(parts)-1] + rrIndex := strings.Index(name, domain) + if rrIndex < 1 { + rrIndex = 1 + } + rr = name[0 : rrIndex-1] } } + if rr == "" { rr = nullHostAlibabaCloud } + return rr, domain } diff --git a/provider/alibaba_cloud_test.go b/provider/alibaba_cloud_test.go index 5d4b97a10..d9d70a1cc 100644 --- a/provider/alibaba_cloud_test.go +++ b/provider/alibaba_cloud_test.go @@ -404,6 +404,16 @@ func TestAlibabaCloudProvider_splitDNSName(t *testing.T) { if rr != "@" || domain != "container-service.top" { t.Errorf("Failed to splitDNSName for %s: rr=%s, domain=%s", endpoint.DNSName, rr, domain) } + endpoint.DNSName = "a.b.container-service.top" + rr, domain = p.splitDNSName(endpoint) + if rr != "a.b" || domain != "container-service.top" { + t.Errorf("Failed to splitDNSName for %s: rr=%s, domain=%s", endpoint.DNSName, rr, domain) + } + endpoint.DNSName = "a.b.c.container-service.top" + rr, domain = p.splitDNSName(endpoint) + if rr != "a.b.c" || domain != "container-service.top" { + t.Errorf("Failed to splitDNSName for %s: rr=%s, domain=%s", endpoint.DNSName, rr, domain) + } } func TestAlibabaCloudProvider_TXTEndpoint(t *testing.T) { diff --git a/provider/aws.go b/provider/aws.go index 6aa0f0412..7ec5e79aa 100644 --- a/provider/aws.go +++ b/provider/aws.go @@ -193,7 +193,7 @@ func (p *AWSProvider) Zones() (map[string]*route53.HostedZone, error) { // wildcardUnescape converts \\052.abc back to *.abc // Route53 stores wildcards escaped: http://docs.aws.amazon.com/Route53/latest/DeveloperGuide/DomainNameFormat.html?shortFooter=true#domain-name-format-asterisk func wildcardUnescape(s string) string { - if strings.HasPrefix(s, "\\052") { + if strings.Contains(s, "\\052") { s = strings.Replace(s, "\\052", "*", 1) } return s @@ -364,6 +364,11 @@ func (p *AWSProvider) newChange(action string, endpoint *endpoint.Endpoint) *rou }, } + rec, err := p.Records() + if err != nil { + log.Infof("getting records failed: %v", err) + } + if isAWSLoadBalancer(endpoint) { evalTargetHealth := p.evaluateTargetHealth if _, ok := endpoint.ProviderSpecific[providerSpecificEvaluateTargetHealth]; ok { @@ -376,6 +381,19 @@ func (p *AWSProvider) newChange(action string, endpoint *endpoint.Endpoint) *rou HostedZoneId: aws.String(canonicalHostedZone(endpoint.Targets[0])), EvaluateTargetHealth: aws.Bool(evalTargetHealth), } + } else if hostedZone := isAWSAlias(endpoint, rec); hostedZone != "" { + zones, err := p.Zones() + if err != nil { + log.Errorf("getting zones failed: %v", err) + } + for _, zone := range zones { + change.ResourceRecordSet.Type = aws.String(route53.RRTypeA) + change.ResourceRecordSet.AliasTarget = &route53.AliasTarget{ + DNSName: aws.String(endpoint.Targets[0]), + HostedZoneId: aws.String(cleanZoneID(*zone.Id)), + EvaluateTargetHealth: aws.Bool(p.evaluateTargetHealth), + } + } } else { change.ResourceRecordSet.Type = aws.String(endpoint.RecordType) if !endpoint.RecordTTL.IsConfigured() { @@ -529,6 +547,21 @@ func isAWSLoadBalancer(ep *endpoint.Endpoint) bool { return false } +// isAWSAlias determines if a given hostname belongs to an AWS Alias record by doing an reverse lookup. +func isAWSAlias(ep *endpoint.Endpoint, addrs []*endpoint.Endpoint) string { + if val, exists := ep.ProviderSpecific["alias"]; ep.RecordType == endpoint.RecordTypeCNAME && exists && val == "true" { + for _, addr := range addrs { + if addr.DNSName == ep.Targets[0] { + if hostedZone := canonicalHostedZone(addr.Targets[0]); hostedZone != "" { + return hostedZone + } + + } + } + } + return "" +} + // canonicalHostedZone returns the matching canonical zone for a given hostname. func canonicalHostedZone(hostname string) string { for suffix, zone := range canonicalHostedZones { @@ -539,3 +572,11 @@ func canonicalHostedZone(hostname string) string { return "" } + +// cleanZoneID removes the "/hostedzone/" prefix +func cleanZoneID(ID string) string { + if strings.HasPrefix(ID, "/hostedzone/") { + ID = strings.TrimPrefix(ID, "/hostedzone/") + } + return ID +} diff --git a/provider/aws_test.go b/provider/aws_test.go index 60c451e99..6852b6df2 100644 --- a/provider/aws_test.go +++ b/provider/aws_test.go @@ -89,7 +89,7 @@ func (r *Route53APIStub) ListResourceRecordSetsPages(input *route53.ListResource // Route53 stores wildcards escaped: http://docs.aws.amazon.com/Route53/latest/DeveloperGuide/DomainNameFormat.html?shortFooter=true#domain-name-format-asterisk func wildcardEscape(s string) string { - if strings.HasPrefix(s, "*") { + if strings.Contains(s, "*") { s = strings.Replace(s, "*", "\\052", 1) } return s @@ -257,6 +257,7 @@ func TestAWSRecords(t *testing.T) { endpoint.NewEndpoint("*.wildcard-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false"), endpoint.NewEndpoint("list-test-alias-evaluate.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true"), endpoint.NewEndpointWithTTL("list-test-multiple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8", "8.8.4.4"), + endpoint.NewEndpointWithTTL("prefix-*.wildcard.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeTXT, endpoint.TTL(recordTTL), "random"), }) records, err := provider.Records() @@ -270,6 +271,7 @@ func TestAWSRecords(t *testing.T) { endpoint.NewEndpoint("*.wildcard-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false"), endpoint.NewEndpoint("list-test-alias-evaluate.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true"), endpoint.NewEndpointWithTTL("list-test-multiple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8", "8.8.4.4"), + endpoint.NewEndpointWithTTL("prefix-*.wildcard.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeTXT, endpoint.TTL(recordTTL), "random"), }) } @@ -819,6 +821,35 @@ func TestAWSisLoadBalancer(t *testing.T) { } } +func TestAWSisAWSAlias(t *testing.T) { + for _, tc := range []struct { + target string + recordType string + alias string + expected string + }{ + {"bar.example.org", endpoint.RecordTypeCNAME, "true", "Z215JYRZR1TBD5"}, + {"foo.example.org", endpoint.RecordTypeCNAME, "true", ""}, + } { + ep := &endpoint.Endpoint{ + Targets: endpoint.Targets{tc.target}, + RecordType: tc.recordType, + ProviderSpecific: map[string]string{"alias": tc.alias}, + } + addrs := []*endpoint.Endpoint{ + { + DNSName: "foo.example.org", + Targets: endpoint.Targets{"foobar.example.org"}, + }, + { + DNSName: "bar.example.org", + Targets: endpoint.Targets{"bar.eu-central-1.elb.amazonaws.com"}, + }, + } + assert.Equal(t, tc.expected, isAWSAlias(ep, addrs)) + } +} + func TestAWSCanonicalHostedZone(t *testing.T) { for _, tc := range []struct { hostname string @@ -929,10 +960,13 @@ func setupAWSRecords(t *testing.T, provider *AWSProvider, endpoints []*endpoint. require.NoError(t, provider.CreateRecords(endpoints)) + escapeAWSRecords(t, provider, "/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do.") + escapeAWSRecords(t, provider, "/hostedzone/zone-2.ext-dns-test-2.teapot.zalan.do.") + escapeAWSRecords(t, provider, "/hostedzone/zone-3.ext-dns-test-2.teapot.zalan.do.") + records, err = provider.Records() require.NoError(t, err) - validateEndpoints(t, records, endpoints) } func listAWSRecords(t *testing.T, client Route53API, zone string) []*route53.ResourceRecordSet { @@ -941,10 +975,7 @@ func listAWSRecords(t *testing.T, client Route53API, zone string) []*route53.Res HostedZoneId: aws.String(zone), }, func(resp *route53.ListResourceRecordSetsOutput, _ bool) bool { for _, recordSet := range resp.ResourceRecordSets { - switch aws.StringValue(recordSet.Type) { - case endpoint.RecordTypeA, endpoint.RecordTypeCNAME: - recordSets = append(recordSets, recordSet) - } + recordSets = append(recordSets, recordSet) } return true })) @@ -974,6 +1005,29 @@ func clearAWSRecords(t *testing.T, provider *AWSProvider, zone string) { } } +// Route53 stores wildcards escaped: http://docs.aws.amazon.com/Route53/latest/DeveloperGuide/DomainNameFormat.html?shortFooter=true#domain-name-format-asterisk +func escapeAWSRecords(t *testing.T, provider *AWSProvider, zone string) { + recordSets := listAWSRecords(t, provider.client, zone) + + changes := make([]*route53.Change, 0, len(recordSets)) + for _, recordSet := range recordSets { + changes = append(changes, &route53.Change{ + Action: aws.String(route53.ChangeActionUpsert), + ResourceRecordSet: recordSet, + }) + } + + if len(changes) != 0 { + _, err := provider.client.ChangeResourceRecordSets(&route53.ChangeResourceRecordSetsInput{ + HostedZoneId: aws.String(zone), + ChangeBatch: &route53.ChangeBatch{ + Changes: changes, + }, + }) + require.NoError(t, err) + } +} + func newAWSProvider(t *testing.T, domainFilter DomainFilter, zoneIDFilter ZoneIDFilter, zoneTypeFilter ZoneTypeFilter, evaluateTargetHealth, dryRun bool, records []*endpoint.Endpoint) (*AWSProvider, *Route53APIStub) { client := NewRoute53APIStub() diff --git a/provider/azure_test.go b/provider/azure_test.go index bdae835e8..e9da5c286 100644 --- a/provider/azure_test.go +++ b/provider/azure_test.go @@ -48,7 +48,7 @@ func createMockZone(zone string, id string) dns.Zone { } func (client *mockZonesClient) ListByResourceGroup(resourceGroupName string, top *int32) (dns.ZoneListResult, error) { - // Don't bother filtering by resouce group or implementing paging since that's the responsibility + // Don't bother filtering by resource group or implementing paging since that's the responsibility // of the Azure DNS service return *client.mockZoneListResult, nil } diff --git a/provider/coredns.go b/provider/coredns.go index 7454e1691..38403ae61 100644 --- a/provider/coredns.go +++ b/provider/coredns.go @@ -17,7 +17,7 @@ limitations under the License. package provider import ( - "container/list" + "context" "crypto/tls" "crypto/x509" "encoding/json" @@ -26,14 +26,12 @@ import ( "io/ioutil" "math/rand" "net" - "net/http" "os" "strings" "time" - etcd "github.com/coreos/etcd/client" + etcdcv3 "github.com/coreos/etcd/clientv3" log "github.com/sirupsen/logrus" - "golang.org/x/net/context" "github.com/kubernetes-incubator/external-dns/endpoint" "github.com/kubernetes-incubator/external-dns/plan" @@ -43,8 +41,17 @@ func init() { rand.Seed(time.Now().UnixNano()) } -// skyDNSClient is an interface to work with SkyDNS service records in etcd -type skyDNSClient interface { +const ( + priority = 10 // default priority when nothing is set + etcdTimeout = 5 * time.Second + + coreDNSPrefix = "/skydns/" + + randomPrefixLabel = "prefix" +) + +// coreDNSClient is an interface to work with CoreDNS service records in etcd +type coreDNSClient interface { GetServices(prefix string) ([]*Service, error) SaveService(value *Service) error DeleteService(key string) error @@ -53,10 +60,10 @@ type skyDNSClient interface { type coreDNSProvider struct { dryRun bool domainFilter DomainFilter - client skyDNSClient + client coreDNSClient } -// Service represents SkyDNS/CoreDNS etcd record +// Service represents CoreDNS etcd record type Service struct { Host string `json:"host,omitempty"` Port int `json:"port,omitempty"` @@ -83,52 +90,58 @@ type Service struct { } type etcdClient struct { - api etcd.KeysAPI + client *etcdcv3.Client + ctx context.Context } -var _ skyDNSClient = etcdClient{} +var _ coreDNSClient = etcdClient{} // GetService return all Service records stored in etcd stored anywhere under the given key (recursively) func (c etcdClient) GetServices(prefix string) ([]*Service, error) { - var result []*Service - opts := &etcd.GetOptions{Recursive: true} - data, err := c.api.Get(context.Background(), prefix, opts) + ctx, cancel := context.WithTimeout(c.ctx, etcdTimeout) + defer cancel() + + path := prefix + r, err := c.client.Get(ctx, path, etcdcv3.WithPrefix()) if err != nil { - if etcd.IsKeyNotFound(err) { - return nil, nil - } return nil, err } - queue := list.New() - queue.PushFront(data.Node) - for queueNode := queue.Front(); queueNode != nil; queueNode = queueNode.Next() { - node := queueNode.Value.(*etcd.Node) - if node.Dir { - for _, childNode := range node.Nodes { - queue.PushBack(childNode) - } + var svcs []*Service + bx := make(map[Service]bool) + for _, n := range r.Kvs { + svc := new(Service) + if err := json.Unmarshal(n.Value, svc); err != nil { + return nil, fmt.Errorf("%s: %s", n.Key, err.Error()) + } + b := Service{Host: svc.Host, Port: svc.Port, Priority: svc.Priority, Weight: svc.Weight, Text: svc.Text, Key: string(n.Key)} + if _, ok := bx[b]; ok { + // skip the service if already added to service list. + // the same service might be found in multiple etcd nodes. continue } - service := &Service{} - err = json.Unmarshal([]byte(node.Value), service) - if err != nil { - log.Error("Cannot parse JSON value ", node.Value) - continue + bx[b] = true + + svc.Key = string(n.Key) + if svc.Priority == 0 { + svc.Priority = priority } - service.Key = node.Key - result = append(result, service) + svcs = append(svcs, svc) } - return result, nil + + return svcs, nil } // SaveService persists service data into etcd func (c etcdClient) SaveService(service *Service) error { + ctx, cancel := context.WithTimeout(c.ctx, etcdTimeout) + defer cancel() + value, err := json.Marshal(&service) if err != nil { return err } - _, err = c.api.Set(context.Background(), service.Key, string(value), nil) + _, err = c.client.Put(ctx, service.Key, string(value)) if err != nil { return err } @@ -137,9 +150,11 @@ func (c etcdClient) SaveService(service *Service) error { // DeleteService deletes service record from etcd func (c etcdClient) DeleteService(key string) error { - _, err := c.api.Delete(context.Background(), key, nil) - return err + ctx, cancel := context.WithTimeout(c.ctx, etcdTimeout) + defer cancel() + _, err := c.client.Delete(ctx, key) + return err } // loads TLS artifacts and builds tls.Clonfig object @@ -186,21 +201,8 @@ func loadRoots(caPath string) (*x509.CertPool, error) { return roots, nil } -// constructs http.Transport object for https protocol -func newHTTPSTransport(cc *tls.Config) *http.Transport { - return &http.Transport{ - Proxy: http.ProxyFromEnvironment, - Dial: (&net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, - }).Dial, - TLSHandshakeTimeout: 10 * time.Second, - TLSClientConfig: cc, - } -} - // builds etcd client config depending on connection scheme and TLS parameters -func getETCDConfig() (*etcd.Config, error) { +func getETCDConfig() (*etcdcv3.Config, error) { etcdURLsStr := os.Getenv("ETCD_URLS") if etcdURLsStr == "" { etcdURLsStr = "http://localhost:2379" @@ -208,7 +210,7 @@ func getETCDConfig() (*etcd.Config, error) { etcdURLs := strings.Split(etcdURLsStr, ",") firstURL := strings.ToLower(etcdURLs[0]) if strings.HasPrefix(firstURL, "http://") { - return &etcd.Config{Endpoints: etcdURLs}, nil + return &etcdcv3.Config{Endpoints: etcdURLs}, nil } else if strings.HasPrefix(firstURL, "https://") { caFile := os.Getenv("ETCD_CA_FILE") certFile := os.Getenv("ETCD_CERT_FILE") @@ -220,9 +222,9 @@ func getETCDConfig() (*etcd.Config, error) { if err != nil { return nil, err } - return &etcd.Config{ + return &etcdcv3.Config{ Endpoints: etcdURLs, - Transport: newHTTPSTransport(tlsConfig), + TLS: tlsConfig, }, nil } else { return nil, errors.New("etcd URLs must start with either http:// or https://") @@ -230,16 +232,16 @@ func getETCDConfig() (*etcd.Config, error) { } //newETCDClient is an etcd client constructor -func newETCDClient() (skyDNSClient, error) { +func newETCDClient() (coreDNSClient, error) { cfg, err := getETCDConfig() if err != nil { return nil, err } - c, err := etcd.New(*cfg) + c, err := etcdcv3.New(*cfg) if err != nil { return nil, err } - return etcdClient{etcd.NewKeysAPI(c)}, nil + return etcdClient{c, context.Background()}, nil } // NewCoreDNSProvider is a CoreDNS provider constructor @@ -255,16 +257,16 @@ func NewCoreDNSProvider(domainFilter DomainFilter, dryRun bool) (Provider, error }, nil } -// Records returns all DNS records found in SkyDNS/CoreDNS etcd backend. Depending on the record fields +// Records returns all DNS records found in CoreDNS etcd backend. Depending on the record fields // it may be mapped to one or two records of type A, CNAME, TXT, A+TXT, CNAME+TXT func (p coreDNSProvider) Records() ([]*endpoint.Endpoint, error) { var result []*endpoint.Endpoint - services, err := p.client.GetServices("/skydns") + services, err := p.client.GetServices(coreDNSPrefix) if err != nil { return nil, err } for _, service := range services { - domains := strings.Split(strings.TrimPrefix(service.Key, "/skydns/"), "/") + domains := strings.Split(strings.TrimPrefix(service.Key, coreDNSPrefix), "/") reverse(domains) dnsName := strings.Join(domains[service.TargetStrip:], ".") if !p.domainFilter.Match(dnsName) { @@ -272,13 +274,14 @@ func (p coreDNSProvider) Records() ([]*endpoint.Endpoint, error) { } prefix := strings.Join(domains[:service.TargetStrip], ".") if service.Host != "" { - ep := endpoint.NewEndpoint( + ep := endpoint.NewEndpointWithTTL( dnsName, guessRecordType(service.Host), + endpoint.TTL(service.TTL), service.Host, ) ep.Labels["originalText"] = service.Text - ep.Labels["prefix"] = prefix + ep.Labels[randomPrefixLabel] = prefix result = append(result, ep) } if service.Text != "" { @@ -287,20 +290,21 @@ func (p coreDNSProvider) Records() ([]*endpoint.Endpoint, error) { endpoint.RecordTypeTXT, service.Text, ) - ep.Labels["prefix"] = prefix + ep.Labels[randomPrefixLabel] = prefix result = append(result, ep) } } return result, nil } -// ApplyChanges stores changes back to etcd converting them to SkyDNS format and aggregating A/CNAME and TXT records +// ApplyChanges stores changes back to etcd converting them to CoreDNS format and aggregating A/CNAME and TXT records func (p coreDNSProvider) ApplyChanges(changes *plan.Changes) error { grouped := map[string][]*endpoint.Endpoint{} for _, ep := range changes.Create { grouped[ep.DNSName] = append(grouped[ep.DNSName], ep) } - for _, ep := range changes.UpdateNew { + for i, ep := range changes.UpdateNew { + ep.Labels[randomPrefixLabel] = changes.UpdateOld[i].Labels[randomPrefixLabel] grouped[ep.DNSName] = append(grouped[ep.DNSName], ep) } for dnsName, group := range grouped { @@ -313,7 +317,7 @@ func (p coreDNSProvider) ApplyChanges(changes *plan.Changes) error { if ep.RecordType == endpoint.RecordTypeTXT { continue } - prefix := ep.Labels["prefix"] + prefix := ep.Labels[randomPrefixLabel] if prefix == "" { prefix = fmt.Sprintf("%08x", rand.Int31()) } @@ -322,6 +326,7 @@ func (p coreDNSProvider) ApplyChanges(changes *plan.Changes) error { Text: ep.Labels["originalText"], Key: etcdKeyFor(prefix + "." + dnsName), TargetStrip: strings.Count(prefix, ".") + 1, + TTL: uint32(ep.RecordTTL), } services = append(services, service) } @@ -331,13 +336,14 @@ func (p coreDNSProvider) ApplyChanges(changes *plan.Changes) error { continue } if index >= len(services) { - prefix := ep.Labels["prefix"] + prefix := ep.Labels[randomPrefixLabel] if prefix == "" { prefix = fmt.Sprintf("%08x", rand.Int31()) } services = append(services, Service{ Key: etcdKeyFor(prefix + "." + dnsName), TargetStrip: strings.Count(prefix, ".") + 1, + TTL: uint32(ep.RecordTTL), }) } services[index].Text = ep.Targets[0] @@ -349,7 +355,7 @@ func (p coreDNSProvider) ApplyChanges(changes *plan.Changes) error { } for _, service := range services { - log.Infof("Add/set key %s to Host=%s, Text=%s", service.Key, service.Host, service.Text) + log.Infof("Add/set key %s to Host=%s, Text=%s, TTL=%d", service.Key, service.Host, service.Text, service.TTL) if !p.dryRun { err := p.client.SaveService(&service) if err != nil { @@ -361,8 +367,8 @@ func (p coreDNSProvider) ApplyChanges(changes *plan.Changes) error { for _, ep := range changes.Delete { dnsName := ep.DNSName - if ep.Labels["prefix"] != "" { - dnsName = ep.Labels["prefix"] + "." + dnsName + if ep.Labels[randomPrefixLabel] != "" { + dnsName = ep.Labels[randomPrefixLabel] + "." + dnsName } key := etcdKeyFor(dnsName) log.Infof("Delete key %s", key) @@ -387,7 +393,7 @@ func guessRecordType(target string) string { func etcdKeyFor(dnsName string) string { domains := strings.Split(dnsName, ".") reverse(domains) - return "/skydns/" + strings.Join(domains, "/") + return coreDNSPrefix + strings.Join(domains, "/") } func reverse(slice []string) { diff --git a/provider/coredns_test.go b/provider/coredns_test.go index 9c4b90ce1..147711743 100644 --- a/provider/coredns_test.go +++ b/provider/coredns_test.go @@ -235,8 +235,6 @@ func TestCoreDNSApplyChanges(t *testing.T) { } validateServices(client.services, expectedServices1, t, 1) - updatedEp := endpoint.NewEndpoint("domain1.local", endpoint.RecordTypeA, "6.6.6.6") - updatedEp.Labels["originalText"] = "string1" changes2 := &plan.Changes{ Create: []*endpoint.Endpoint{ endpoint.NewEndpoint("domain3.local", endpoint.RecordTypeA, "7.7.7.7"), @@ -245,6 +243,12 @@ func TestCoreDNSApplyChanges(t *testing.T) { endpoint.NewEndpoint("domain1.local", "A", "6.6.6.6"), }, } + records, _ := coredns.Records() + for _, ep := range records { + if ep.DNSName == "domain1.local" { + changes2.UpdateOld = append(changes2.UpdateOld, ep) + } + } applyServiceChanges(coredns, changes2) expectedServices2 := map[string]*Service{ diff --git a/provider/dyn.go b/provider/dyn.go index 7bb2332dc..36eefdbfb 100644 --- a/provider/dyn.go +++ b/provider/dyn.go @@ -564,7 +564,7 @@ func (d *dynProviderState) commit(client *dynect.Client) error { err = apiRetryLoop(func() error { return client.Do("PUT", fmt.Sprintf("Zone/%s/", zone), &zonePublish, &response) }) - log.Infof("Commiting changes for zone %s: %+v", zone, errorOrValue(err, &response)) + log.Infof("Committing changes for zone %s: %+v", zone, errorOrValue(err, &response)) } switch len(errs) { @@ -597,7 +597,7 @@ func (d *dynProviderState) Records() ([]*endpoint.Endpoint, error) { serial, err := d.fetchZoneSerial(client, zone) if err != nil { if strings.Index(err.Error(), "404 Not Found") >= 0 { - log.Infof("Ignore zone %s as it does not exists", zone) + log.Infof("Ignore zone %s as it does not exist", zone) continue } diff --git a/provider/oci.go b/provider/oci.go index 5ff1ba8f8..aa0c03411 100644 --- a/provider/oci.go +++ b/provider/oci.go @@ -290,7 +290,7 @@ func operationsByZone(zones map[string]*dns.ZoneSummary, ops []dns.RecordOperati } } - // Remove zones that don't have have any changes. + // Remove zones that don't have any changes. for zone, ops := range changes { if len(ops) == 0 { delete(changes, zone) diff --git a/provider/pdns.go b/provider/pdns.go index 862dab902..8b33a4c96 100644 --- a/provider/pdns.go +++ b/provider/pdns.go @@ -132,15 +132,17 @@ func stringifyHTTPResponseBody(r *http.Response) (body string) { // well as mock APIClients used in testing type PDNSAPIProvider interface { ListZones() ([]pgo.Zone, *http.Response, error) + PartitionZones(zones []pgo.Zone) ([]pgo.Zone, []pgo.Zone) ListZone(zoneID string) (pgo.Zone, *http.Response, error) PatchZone(zoneID string, zoneStruct pgo.Zone) (*http.Response, error) } // PDNSAPIClient : Struct that encapsulates all the PowerDNS specific implementation details type PDNSAPIClient struct { - dryRun bool - authCtx context.Context - client *pgo.APIClient + dryRun bool + authCtx context.Context + client *pgo.APIClient + domainFilter DomainFilter } // ListZones : Method returns all enabled zones from PowerDNS @@ -153,7 +155,6 @@ func (c *PDNSAPIClient) ListZones() (zones []pgo.Zone, resp *http.Response, err log.Debugf("Retrying ListZones() ... %d", i) time.Sleep(retryAfterTime * (1 << uint(i))) continue - } return zones, resp, err } @@ -163,6 +164,22 @@ func (c *PDNSAPIClient) ListZones() (zones []pgo.Zone, resp *http.Response, err } +// PartitionZones : Method returns a slice of zones that adhere to the domain filter and a slice of ones that does not adhere to the filter +func (c *PDNSAPIClient) PartitionZones(zones []pgo.Zone) (filteredZones []pgo.Zone, residualZones []pgo.Zone) { + if c.domainFilter.IsConfigured() { + for _, zone := range zones { + if c.domainFilter.Match(zone.Name) { + filteredZones = append(filteredZones, zone) + } else { + residualZones = append(residualZones, zone) + } + } + } else { + residualZones = zones + } + return filteredZones, residualZones +} + // ListZone : Method returns the details of a specific zone from PowerDNS // ref: https://doc.powerdns.com/authoritative/http-api/zone.html#get--servers-server_id-zones-zone_id func (c *PDNSAPIClient) ListZone(zoneID string) (zone pgo.Zone, resp *http.Response, err error) { @@ -216,10 +233,6 @@ func NewPDNSProvider(config PDNSConfig) (*PDNSProvider, error) { return nil, errors.New("Missing API Key for PDNS. Specify using --pdns-api-key=") } - // The default for when no --domain-filter is passed is [""], instead of [], so we check accordingly. - if len(config.DomainFilter.filters) != 1 && config.DomainFilter.filters[0] != "" { - return nil, errors.New("PDNS Provider does not support domain filter") - } // We do not support dry running, exit safely instead of surprising the user // TODO: Add Dry Run support if config.DryRun { @@ -238,9 +251,10 @@ func NewPDNSProvider(config PDNSConfig) (*PDNSProvider, error) { provider := &PDNSProvider{ client: &PDNSAPIClient{ - dryRun: config.DryRun, - authCtx: context.WithValue(context.TODO(), pgo.ContextAPIKey, pgo.APIKey{Key: config.APIKey}), - client: pgo.NewAPIClient(pdnsClientConfig), + dryRun: config.DryRun, + authCtx: context.WithValue(context.TODO(), pgo.ContextAPIKey, pgo.APIKey{Key: config.APIKey}), + client: pgo.NewAPIClient(pdnsClientConfig), + domainFilter: config.DomainFilter, }, } @@ -281,22 +295,23 @@ func (p *PDNSProvider) ConvertEndpointsToZones(eps []*endpoint.Endpoint, changet if err != nil { return nil, err } + filteredZones, residualZones := p.client.PartitionZones(zones) // Sort the zone by length of the name in descending order, we use this // property later to ensure we add a record to the longest matching zone - sort.SliceStable(zones, func(i, j int) bool { return len(zones[i].Name) > len(zones[j].Name) }) + sort.SliceStable(filteredZones, func(i, j int) bool { return len(filteredZones[i].Name) > len(filteredZones[j].Name) }) - // NOTE: Complexity of this loop is O(Zones*Endpoints). + // NOTE: Complexity of this loop is O(FilteredZones*Endpoints). // A possibly faster implementation would be a search of the reversed // DNSName in a trie of Zone names, which should be O(Endpoints), but at this point it's not // necessary. - for _, zone := range zones { + for _, zone := range filteredZones { zone.Rrsets = []pgo.RrSet{} for i := 0; i < len(endpoints); { ep := endpoints[i] dnsname := ensureTrailingDot(ep.DNSName) - if strings.HasSuffix(dnsname, zone.Name) { + if dnsname == zone.Name || strings.HasSuffix(dnsname, "."+zone.Name) { // The assumption here is that there will only ever be one target // per (ep.DNSName, ep.RecordType) tuple, which holds true for // external-dns v5.0.0-alpha onwards @@ -321,7 +336,7 @@ func (p *PDNSProvider) ConvertEndpointsToZones(eps []*endpoint.Endpoint, changet return nil, errors.New("Value of record TTL overflows, limited to int32") } if ep.RecordTTL == 0 { - // No TTL was sepecified for the record, we use the default + // No TTL was specified for the record, we use the default rrset.Ttl = int32(defaultTTL) } else { rrset.Ttl = int32(ep.RecordTTL) @@ -345,7 +360,23 @@ func (p *PDNSProvider) ConvertEndpointsToZones(eps []*endpoint.Endpoint, changet } - // If we still have some endpoints left, it means we couldn't find a matching zone for them + // residualZones is unsorted by name length like its counterpart + // since we only care to remove endpoints that do not match domain filter + for _, zone := range residualZones { + for i := 0; i < len(endpoints); { + ep := endpoints[i] + dnsname := ensureTrailingDot(ep.DNSName) + if dnsname == zone.Name || strings.HasSuffix(dnsname, "."+zone.Name) { + // "pop" endpoint if it's matched to a residual zone... essentially a no-op + log.Debugf("Ignoring Endpoint because it was matched to a zone that was not specified within Domain Filter(s): %s", dnsname) + endpoints = append(endpoints[0:i], endpoints[i+1:]...) + } else { + i++ + } + } + } + + // If we still have some endpoints left, it means we couldn't find a matching zone (filtered or residual) for them // We warn instead of hard fail here because we don't want a misconfig to cause everything to go down if len(endpoints) > 0 { log.Warnf("No matching zones were found for the following endpoints: %+v", endpoints) @@ -387,8 +418,9 @@ func (p *PDNSProvider) Records() (endpoints []*endpoint.Endpoint, _ error) { if err != nil { return nil, err } + filteredZones, _ := p.client.PartitionZones(zones) - for _, zone := range zones { + for _, zone := range filteredZones { z, _, err := p.client.ListZone(zone.Id) if err != nil { log.Warnf("Unable to fetch Records") diff --git a/provider/pdns_test.go b/provider/pdns_test.go index 0351d3e06..c7c5b592c 100644 --- a/provider/pdns_test.go +++ b/provider/pdns_test.go @@ -158,6 +158,18 @@ var ( endpoint.NewEndpointWithTTL("abcd.mock.noexist", endpoint.RecordTypeA, endpoint.TTL(300), "9.9.9.9"), endpoint.NewEndpointWithTTL("abcd.mock.noexist", endpoint.RecordTypeTXT, endpoint.TTL(300), "\"heritage=external-dns,external-dns/owner=tower-pdns\""), } + endpointsMultipleZonesWithLongRecordNotInDomainFilter = []*endpoint.Endpoint{ + endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeA, endpoint.TTL(300), "8.8.8.8"), + endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeTXT, endpoint.TTL(300), "\"heritage=external-dns,external-dns/owner=tower-pdns\""), + endpoint.NewEndpointWithTTL("a.very.long.domainname.example.com", endpoint.RecordTypeA, endpoint.TTL(300), "9.9.9.9"), + endpoint.NewEndpointWithTTL("a.very.long.domainname.example.com", endpoint.RecordTypeTXT, endpoint.TTL(300), "\"heritage=external-dns,external-dns/owner=tower-pdns\""), + } + endpointsMultipleZonesWithSimilarRecordNotInDomainFilter = []*endpoint.Endpoint{ + endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeA, endpoint.TTL(300), "8.8.8.8"), + endpoint.NewEndpointWithTTL("example.com", endpoint.RecordTypeTXT, endpoint.TTL(300), "\"heritage=external-dns,external-dns/owner=tower-pdns\""), + endpoint.NewEndpointWithTTL("test.simexample.com", endpoint.RecordTypeA, endpoint.TTL(300), "9.9.9.9"), + endpoint.NewEndpointWithTTL("test.simexample.com", endpoint.RecordTypeTXT, endpoint.TTL(300), "\"heritage=external-dns,external-dns/owner=tower-pdns\""), + } ZoneEmpty = pgo.Zone{ // Opaque zone id (string), assigned by the server, should not be interpreted by the application. Guaranteed to be safe for embedding in URLs. @@ -174,6 +186,15 @@ var ( Rrsets: []pgo.RrSet{}, } + ZoneEmptySimilar = pgo.Zone{ + Id: "simexample.com.", + Name: "simexample.com.", + Type_: "Zone", + Url: "/api/v1/servers/localhost/zones/simexample.com.", + Kind: "Native", + Rrsets: []pgo.RrSet{}, + } + ZoneEmptyLong = pgo.Zone{ Id: "long.domainname.example.com.", Name: "long.domainname.example.com.", @@ -239,6 +260,72 @@ var ( }, } + ZoneEmptyToSimplePatchLongRecordIgnoredInDomainFilter = pgo.Zone{ + Id: "example.com.", + Name: "example.com.", + Type_: "Zone", + Url: "/api/v1/servers/localhost/zones/example.com.", + Kind: "Native", + Rrsets: []pgo.RrSet{ + { + Name: "a.very.long.domainname.example.com.", + Type_: "A", + Ttl: 300, + Changetype: "REPLACE", + Records: []pgo.Record{ + { + Content: "9.9.9.9", + Disabled: false, + SetPtr: false, + }, + }, + Comments: []pgo.Comment(nil), + }, + { + Name: "a.very.long.domainname.example.com.", + Type_: "TXT", + Ttl: 300, + Changetype: "REPLACE", + Records: []pgo.Record{ + { + Content: "\"heritage=external-dns,external-dns/owner=tower-pdns\"", + Disabled: false, + SetPtr: false, + }, + }, + Comments: []pgo.Comment(nil), + }, + { + Name: "example.com.", + Type_: "A", + Ttl: 300, + Changetype: "REPLACE", + Records: []pgo.Record{ + { + Content: "8.8.8.8", + Disabled: false, + SetPtr: false, + }, + }, + Comments: []pgo.Comment(nil), + }, + { + Name: "example.com.", + Type_: "TXT", + Ttl: 300, + Changetype: "REPLACE", + Records: []pgo.Record{ + { + Content: "\"heritage=external-dns,external-dns/owner=tower-pdns\"", + Disabled: false, + SetPtr: false, + }, + }, + Comments: []pgo.Comment(nil), + }, + }, + } + ZoneEmptyToLongPatch = pgo.Zone{ Id: "long.domainname.example.com.", Name: "long.domainname.example.com.", @@ -398,6 +485,9 @@ type PDNSAPIClientStub struct { func (c *PDNSAPIClientStub) ListZones() ([]pgo.Zone, *http.Response, error) { return []pgo.Zone{ZoneMixed}, nil, nil } +func (c *PDNSAPIClientStub) PartitionZones(zones []pgo.Zone) ([]pgo.Zone, []pgo.Zone) { + return zones, nil +} func (c *PDNSAPIClientStub) ListZone(zoneID string) (pgo.Zone, *http.Response, error) { return ZoneMixed, nil, nil } @@ -415,6 +505,9 @@ type PDNSAPIClientStubEmptyZones struct { func (c *PDNSAPIClientStubEmptyZones) ListZones() ([]pgo.Zone, *http.Response, error) { return []pgo.Zone{ZoneEmpty, ZoneEmptyLong, ZoneEmpty2}, nil, nil } +func (c *PDNSAPIClientStubEmptyZones) PartitionZones(zones []pgo.Zone) ([]pgo.Zone, []pgo.Zone) { + return zones, nil +} func (c *PDNSAPIClientStubEmptyZones) ListZone(zoneID string) (pgo.Zone, *http.Response, error) { if strings.Contains(zoneID, "example.com") { @@ -422,7 +515,7 @@ func (c *PDNSAPIClientStubEmptyZones) ListZone(zoneID string) (pgo.Zone, *http.R } else if strings.Contains(zoneID, "mock.test") { return ZoneEmpty2, nil, nil } else if strings.Contains(zoneID, "long.domainname.example.com") { - return ZoneEmpty2, nil, nil + return ZoneEmptyLong, nil, nil } return pgo.Zone{}, nil, nil @@ -469,6 +562,37 @@ func (c *PDNSAPIClientStubListZonesFailure) ListZones() ([]pgo.Zone, *http.Respo return []pgo.Zone{}, nil, errors.New("Generic PDNS Error") } +/******************************************************************************/ +// API that returns zone partitions given DomainFilter(s) +type PDNSAPIClientStubPartitionZones struct { + // Anonymous struct for composition + PDNSAPIClientStubEmptyZones +} + +func (c *PDNSAPIClientStubPartitionZones) ListZones() ([]pgo.Zone, *http.Response, error) { + return []pgo.Zone{ZoneEmpty, ZoneEmptyLong, ZoneEmpty2, ZoneEmptySimilar}, nil, nil +} + +func (c *PDNSAPIClientStubPartitionZones) ListZone(zoneID string) (pgo.Zone, *http.Response, error) { + + if strings.Contains(zoneID, "example.com") { + return ZoneEmpty, nil, nil + } else if strings.Contains(zoneID, "mock.test") { + return ZoneEmpty2, nil, nil + } else if strings.Contains(zoneID, "long.domainname.example.com") { + return ZoneEmptyLong, nil, nil + } else if strings.Contains(zoneID, "simexample.com") { + return ZoneEmptySimilar, nil, nil + } + return pgo.Zone{}, nil, nil +} + +// Just overwrite the ListZones method to introduce a failure +func (c *PDNSAPIClientStubPartitionZones) PartitionZones(zones []pgo.Zone) ([]pgo.Zone, []pgo.Zone) { + return []pgo.Zone{ZoneEmpty}, []pgo.Zone{ZoneEmptyLong, ZoneEmpty2} + +} + /******************************************************************************/ type NewPDNSProviderTestSuite struct { @@ -488,7 +612,7 @@ func (suite *NewPDNSProviderTestSuite) TestPDNSProviderCreate() { APIKey: "foo", DomainFilter: NewDomainFilter([]string{"example.com", "example.org"}), }) - assert.Error(suite.T(), err, "--domainfilter should raise an error") + assert.Nil(suite.T(), err, "--domain-filter should raise no error") _, err = NewPDNSProvider(PDNSConfig{ Server: "http://localhost:8081", @@ -711,6 +835,51 @@ func (suite *NewPDNSProviderTestSuite) TestPDNSConvertEndpointsToZones() { } } +func (suite *NewPDNSProviderTestSuite) TestPDNSConvertEndpointsToZonesPartitionZones() { + // Test DomainFilters + p := &PDNSProvider{ + client: &PDNSAPIClientStubPartitionZones{}, + } + + // Check inserting endpoints from a single zone which is specified in DomainFilter + zlist, err := p.ConvertEndpointsToZones(endpointsSimpleRecord, PdnsReplace) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), []pgo.Zone{ZoneEmptyToSimplePatch}, zlist) + + // Check deleting endpoints from a single zone which is specified in DomainFilter + zlist, err = p.ConvertEndpointsToZones(endpointsSimpleRecord, PdnsDelete) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), []pgo.Zone{ZoneEmptyToSimpleDelete}, zlist) + + // Check endpoints from multiple zones # which one is specified in DomainFilter and one is not + zlist, err = p.ConvertEndpointsToZones(endpointsMultipleZones, PdnsReplace) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), []pgo.Zone{ZoneEmptyToSimplePatch}, zlist) + + // Check endpoints from multiple zones where some endpoints which don't exist and one that does + // and is part of DomainFilter + zlist, err = p.ConvertEndpointsToZones(endpointsMultipleZonesWithNoExist, PdnsReplace) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), []pgo.Zone{ZoneEmptyToSimplePatch}, zlist) + + // Check endpoints from a zone that does not exist + zlist, err = p.ConvertEndpointsToZones(endpointsNonexistantZone, PdnsReplace) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), []pgo.Zone{}, zlist) + + // Check endpoints that match multiple zones (one longer than other), is assigned to the right zone when the longer + // zone is not part of the DomainFilter + zlist, err = p.ConvertEndpointsToZones(endpointsMultipleZonesWithLongRecordNotInDomainFilter, PdnsReplace) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), []pgo.Zone{ZoneEmptyToSimplePatchLongRecordIgnoredInDomainFilter}, zlist) + + // Check endpoints that match multiple zones (one longer than other and one is very similar) + // is assigned to the right zone when the similar zone is not part of the DomainFilter + zlist, err = p.ConvertEndpointsToZones(endpointsMultipleZonesWithSimilarRecordNotInDomainFilter, PdnsReplace) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), []pgo.Zone{ZoneEmptyToSimplePatch}, zlist) +} + func (suite *NewPDNSProviderTestSuite) TestPDNSmutateRecords() { // Function definition: mutateRecords(endpoints []*endpoint.Endpoint, changetype pdnsChangeType) error @@ -742,6 +911,7 @@ func (suite *NewPDNSProviderTestSuite) TestPDNSmutateRecords() { assert.NotNil(suite.T(), err) } + func TestNewPDNSProviderTestSuite(t *testing.T) { suite.Run(t, new(NewPDNSProviderTestSuite)) } diff --git a/scripts/update_route53_k8s_txt_owner.py b/scripts/update_route53_k8s_txt_owner.py new file mode 100644 index 000000000..f1a0c7056 --- /dev/null +++ b/scripts/update_route53_k8s_txt_owner.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python + +# Copyright 2018 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. + +# This is a script that we wrote to try to help the migration over to using external-dns. +# This script looks at kubernetes ingresses and services (which are the two things we have +# external-dns looking at) and compares them to existing TXT and A records in route53 to +# find out where there are gaps. It then assigns the heritage and owner TXT records where +# needed so external-dns can take over managing those resources. You can modify the script +# to only look at one or the other if needed. +# +# pip install kubernetes boto3 + +import boto3 +from kubernetes import client, config + +# replace with your hosted zone id +hosted_zone_id = '' +# replace with your txt-owner-id you are using +# inside of your external-dns controller +txt_owner_id = '' + +# change to false if you have external-dns not looking at services +external_dns_manages_services = True + +# change to false if you have external-dns not looking at ingresses +external_dns_manages_ingresses = True + +config.load_kube_config() + +# grab all the domains that k8s thinks it is going to +# manage (services with domainName specified and +# ingress hosts) +k8s_domains = [] + +if external_dns_manages_services: + v1 = client.CoreV1Api() + svcs = v1.list_service_for_all_namespaces() + for i in svcs.items: + annotations = i.metadata.annotations + if annotations is not None and 'domainName' in annotations: + k8s_domains.extend(annotations['domainName'].split(',')) + +if external_dns_manages_ingresses: + ev1 = client.ExtensionsV1beta1Api() + ings = ev1.list_ingress_for_all_namespaces() + for i in ings.items: + for r in i.spec.rules: + if r.host not in k8s_domains: + k8s_domains.append(r.host) + + +r53client = boto3.client('route53') + +# grab the existing route53 domains and identify gaps where a domain may be +# missing a txt record pair +existing_r53_txt_domains=[] +existing_r53_domains=[] +has_next = True +next_record_name, next_record_type='','' + +while has_next: + if next_record_name is not '' and next_record_type is not '': + resource_records = r53client.list_resource_record_sets(HostedZoneId=hosted_zone_id, + StartRecordName=next_record_name, + StartRecordType=next_record_type) + else: + resource_records = r53client.list_resource_record_sets(HostedZoneId=hosted_zone_id) + + for r in resource_records['ResourceRecordSets']: + if r['Type'] == 'TXT': + existing_r53_txt_domains.append(r['Name'][:-1]) + elif r['Type'] == 'A': + existing_r53_domains.append(r['Name'][:-1]) + has_next = resource_records['IsTruncated'] + if has_next: + next_record_name, next_record_type = resource_records['NextRecordName'], resource_records['NextRecordType'] + +# grab only the domains in route53 that kubernetes is managing +r53_k8s_domains = [r for r in k8s_domains if r in existing_r53_domains] +# from those find the ones that do not have matching txt entries +missing_k8s_txt = [r for r in r53_k8s_domains if r not in existing_r53_txt_domains] + +# make the change batch for the route53 call, modify this as needed +change_batch=[] +for r in missing_k8s_txt: + change_batch.append( + { + 'Action': 'CREATE', + 'ResourceRecordSet': { + 'Name': r, + 'Type': 'TXT', + 'TTL': 300, + 'ResourceRecords': [ + { + 'Value': '\heritage=external-dns,owner="' + txt_owner_id + '\"' + }, + ] + } + }) + +print('This will create the following resources') +print(change_batch) +response = input("Good to go? ") + +if response.lower() in ['y', 'yes', 'yup', 'ok', 'sure', 'why not', 'why not?']: + print('Updating route53') + change_response = r53client.change_resource_record_sets( + HostedZoneId=hosted_zone_id, + ChangeBatch={ + 'Changes': change_batch + }) + print('Submitted change request to route53. Details below.') + print(change_response) +else: + print('No changes were made') diff --git a/source/gateway.go b/source/gateway.go index 09a77800f..9d34c16ca 100644 --- a/source/gateway.go +++ b/source/gateway.go @@ -175,12 +175,14 @@ func (sc *gatewaySource) endpointsFromTemplate(config *istiomodel.Config) ([]*en } } + providerSpecific := getProviderSpecificAnnotations(config.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)...) + endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific)...) } return endpoints, nil } @@ -259,12 +261,14 @@ func (sc *gatewaySource) endpointsFromGatewayConfig(config istiomodel.Config) ([ gateway := config.Spec.(*istionetworking.Gateway) + providerSpecific := getProviderSpecificAnnotations(config.Annotations) + for _, server := range gateway.Servers { for _, host := range server.Hosts { if host == "" { continue } - endpoints = append(endpoints, endpointsForHostname(host, targets, ttl)...) + endpoints = append(endpoints, endpointsForHostname(host, targets, ttl, providerSpecific)...) } } @@ -272,7 +276,7 @@ func (sc *gatewaySource) endpointsFromGatewayConfig(config istiomodel.Config) ([ if !sc.ignoreHostnameAnnotation { hostnameList := getHostnamesFromAnnotations(config.Annotations) for _, hostname := range hostnameList { - endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl)...) + endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific)...) } } diff --git a/source/ingress.go b/source/ingress.go index c78c87fd9..f522b5242 100644 --- a/source/ingress.go +++ b/source/ingress.go @@ -148,12 +148,14 @@ func (sc *ingressSource) endpointsFromTemplate(ing *v1beta1.Ingress) ([]*endpoin targets = targetsFromIngressStatus(ing.Status) } + providerSpecific := getProviderSpecificAnnotations(ing.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)...) + endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific)...) } return endpoints, nil } @@ -210,11 +212,13 @@ func endpointsFromIngress(ing *v1beta1.Ingress, ignoreHostnameAnnotation bool) [ targets = targetsFromIngressStatus(ing.Status) } + providerSpecific := getProviderSpecificAnnotations(ing.Annotations) + for _, rule := range ing.Spec.Rules { if rule.Host == "" { continue } - endpoints = append(endpoints, endpointsForHostname(rule.Host, targets, ttl)...) + endpoints = append(endpoints, endpointsForHostname(rule.Host, targets, ttl, providerSpecific)...) } for _, tls := range ing.Spec.TLS { @@ -222,7 +226,7 @@ func endpointsFromIngress(ing *v1beta1.Ingress, ignoreHostnameAnnotation bool) [ if host == "" { continue } - endpoints = append(endpoints, endpointsForHostname(host, targets, ttl)...) + endpoints = append(endpoints, endpointsForHostname(host, targets, ttl, providerSpecific)...) } } @@ -230,7 +234,7 @@ func endpointsFromIngress(ing *v1beta1.Ingress, ignoreHostnameAnnotation bool) [ if !ignoreHostnameAnnotation { hostnameList := getHostnamesFromAnnotations(ing.Annotations) for _, hostname := range hostnameList { - endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl)...) + endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific)...) } } return endpoints diff --git a/source/ingress_test.go b/source/ingress_test.go index 13b77735e..2f29f8020 100644 --- a/source/ingress_test.go +++ b/source/ingress_test.go @@ -820,6 +820,52 @@ func testIngressEndpoints(t *testing.T) { }, }, }, + { + title: "ingress rules with alias and target annotation", + targetNamespace: "", + ingressItems: []fakeIngress{ + { + name: "fake1", + namespace: namespace, + annotations: map[string]string{ + targetAnnotationKey: "ingress-target.com", + aliasAnnotationKey: "true", + }, + dnsnames: []string{"example.org"}, + ips: []string{}, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "example.org", + Targets: endpoint.Targets{"ingress-target.com"}, + RecordType: endpoint.RecordTypeCNAME, + }, + }, + }, + { + title: "ingress rules with alias set false and target annotation", + targetNamespace: "", + ingressItems: []fakeIngress{ + { + name: "fake1", + namespace: namespace, + annotations: map[string]string{ + targetAnnotationKey: "ingress-target.com", + aliasAnnotationKey: "false", + }, + dnsnames: []string{"example.org"}, + ips: []string{}, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "example.org", + Targets: endpoint.Targets{"ingress-target.com"}, + RecordType: endpoint.RecordTypeCNAME, + }, + }, + }, { title: "template for ingress with annotation", targetNamespace: "", diff --git a/source/source.go b/source/source.go index 14f99fe5c..338397dfc 100644 --- a/source/source.go +++ b/source/source.go @@ -35,6 +35,8 @@ const ( targetAnnotationKey = "external-dns.alpha.kubernetes.io/target" // The annotation used for defining the desired DNS record TTL ttlAnnotationKey = "external-dns.alpha.kubernetes.io/ttl" + // The annotation used for switching to the alias record types e. g. AWS Alias records instead of a normal CNAME + aliasAnnotationKey = "external-dns.alpha.kubernetes.io/alias" // The value of the controller annotation so that we feel responsible controllerAnnotationValue = "dns-controller" ) @@ -74,6 +76,18 @@ func getHostnamesFromAnnotations(annotations map[string]string) []string { return strings.Split(strings.Replace(hostnameAnnotation, " ", "", -1), ",") } +func getAliasFromAnnotations(annotations map[string]string) bool { + aliasAnnotation, exists := annotations[aliasAnnotationKey] + return exists && aliasAnnotation == "true" +} + +func getProviderSpecificAnnotations(annotations map[string]string) endpoint.ProviderSpecific { + if getAliasFromAnnotations(annotations) { + return map[string]string{"alias": "true"} + } + return map[string]string{} +} + // getTargetsFromTargetAnnotation gets endpoints from optional "target" annotation. // Returns empty endpoints array if none are found. func getTargetsFromTargetAnnotation(annotations map[string]string) endpoint.Targets { @@ -102,7 +116,7 @@ func suitableType(target string) string { } // endpointsForHostname returns the endpoint objects for each host-target combination. -func endpointsForHostname(hostname string, targets endpoint.Targets, ttl endpoint.TTL) []*endpoint.Endpoint { +func endpointsForHostname(hostname string, targets endpoint.Targets, ttl endpoint.TTL, providerSpecific endpoint.ProviderSpecific) []*endpoint.Endpoint { var endpoints []*endpoint.Endpoint var aTargets endpoint.Targets @@ -119,22 +133,24 @@ func endpointsForHostname(hostname string, targets endpoint.Targets, ttl endpoin if len(aTargets) > 0 { epA := &endpoint.Endpoint{ - DNSName: strings.TrimSuffix(hostname, "."), - Targets: aTargets, - RecordTTL: ttl, - RecordType: endpoint.RecordTypeA, - Labels: endpoint.NewLabels(), + DNSName: strings.TrimSuffix(hostname, "."), + Targets: aTargets, + RecordTTL: ttl, + RecordType: endpoint.RecordTypeA, + Labels: endpoint.NewLabels(), + ProviderSpecific: providerSpecific, } endpoints = append(endpoints, epA) } if len(cnameTargets) > 0 { epCNAME := &endpoint.Endpoint{ - DNSName: strings.TrimSuffix(hostname, "."), - Targets: cnameTargets, - RecordTTL: ttl, - RecordType: endpoint.RecordTypeCNAME, - Labels: endpoint.NewLabels(), + DNSName: strings.TrimSuffix(hostname, "."), + Targets: cnameTargets, + RecordTTL: ttl, + RecordType: endpoint.RecordTypeCNAME, + Labels: endpoint.NewLabels(), + ProviderSpecific: providerSpecific, } endpoints = append(endpoints, epCNAME) }