Merge branch 'master' of github.com:kubernetes-incubator/external-dns into feature/ignore-annotations

This commit is contained in:
Anand Patel 2018-11-16 14:21:23 -03:00
commit 230113c7b8
29 changed files with 937 additions and 260 deletions

View File

@ -23,7 +23,7 @@ RUN make build
# final image # final image
FROM registry.opensource.zalan.do/stups/alpine:latest FROM registry.opensource.zalan.do/stups/alpine:latest
MAINTAINER Team Teapot @ Zalando SE <team-teapot@zalando.de> LABEL maintainer="Team Teapot @ Zalando SE <team-teapot@zalando.de>"
COPY --from=builder /go/src/github.com/kubernetes-incubator/external-dns/build/external-dns /bin/external-dns COPY --from=builder /go/src/github.com/kubernetes-incubator/external-dns/build/external-dns /bin/external-dns

158
Gopkg.lock generated
View File

@ -2,7 +2,7 @@
[[projects]] [[projects]]
digest = "1:e94ea655a0038d2274be202f77a2ea0eb2d3f74dfee674fd5d1f541e81008039" digest = "1:ae9d0182a5cf7dbb025a8fc5821234cc1f26ca342fc41d951a99f71b9adc1b87"
name = "cloud.google.com/go" name = "cloud.google.com/go"
packages = [ packages = [
"compute/metadata", "compute/metadata",
@ -12,7 +12,7 @@
revision = "3b1ae45394a234c385be014e9a488f2bb6eef821" revision = "3b1ae45394a234c385be014e9a488f2bb6eef821"
[[projects]] [[projects]]
digest = "1:b341fb465b057e991b166d073b35a224f5a84228e5ef7e40b4da7a70c152e7ec" digest = "1:fd38e3b8c27cab6561a7d2e8557205c3ca5c57cbb6d3a79e10f22e73e84fd776"
name = "github.com/Azure/azure-sdk-for-go" name = "github.com/Azure/azure-sdk-for-go"
packages = ["arm/dns"] packages = ["arm/dns"]
pruneopts = "" pruneopts = ""
@ -20,7 +20,7 @@
version = "v10.0.4-beta" version = "v10.0.4-beta"
[[projects]] [[projects]]
digest = "1:767f5f5dd4fa8e4f7f206726361d29aa0f7622b0bb8294b73d071864368c0d6b" digest = "1:f719ef698feb8da2923ebda9a8d553b977320b02182f3797e185202e588a94b1"
name = "github.com/Azure/go-autorest" name = "github.com/Azure/go-autorest"
packages = [ packages = [
"autorest", "autorest",
@ -34,7 +34,7 @@
version = "v10.9.0" version = "v10.9.0"
[[projects]] [[projects]]
digest = "1:283a95024c33e84b23f24b1b47e3157ff2df2517d786a2e17bb0e6e4955e94e4" digest = "1:7dc69d1597e4773ec5f64e5c078d55f0f011bb05ec0435346d0649ad978a23fd"
name = "github.com/alecthomas/kingpin" name = "github.com/alecthomas/kingpin"
packages = ["."] packages = ["."]
pruneopts = "" pruneopts = ""
@ -43,7 +43,7 @@
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:1399282ad03ac819f0e8a747c888407c5c98bb497d33821a7047c7bae667ede0" digest = "1:a74730e052a45a3fab1d310fdef2ec17ae3d6af16228421e238320846f2aaec8"
name = "github.com/alecthomas/template" name = "github.com/alecthomas/template"
packages = [ packages = [
".", ".",
@ -61,7 +61,7 @@
revision = "2efee857e7cfd4f3d0138cc3cbb1b4966962b93a" revision = "2efee857e7cfd4f3d0138cc3cbb1b4966962b93a"
[[projects]] [[projects]]
digest = "1:7b6c017b0290ccf1dd98c47a51e1db8b72b0863b6c7c52ddaa5a0d894aa3c2fc" digest = "1:d2dc5d0ccc137594ea6fb3870964967b96b43daac19b8093930c7b02873fd5ca"
name = "github.com/aliyun/alibaba-cloud-sdk-go" name = "github.com/aliyun/alibaba-cloud-sdk-go"
packages = [ packages = [
"sdk", "sdk",
@ -81,7 +81,7 @@
version = "1.27.7" version = "1.27.7"
[[projects]] [[projects]]
digest = "1:f04a72eefe1c7adec1dce30e099cec1e5fea8903a66e2db25bbbdfa66915428d" digest = "1:1c82dd6a02941a3c4f23a32eca182717ab79691939e97d6b971466b780f06eea"
name = "github.com/aws/aws-sdk-go" name = "github.com/aws/aws-sdk-go"
packages = [ packages = [
"aws", "aws",
@ -121,14 +121,14 @@
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:d20bdb6bf44087574af3139835946875bb098440426785282c741865b7bc66d3" digest = "1:0c5485088ce274fac2e931c1b979f2619345097b39d91af3239977114adf0320"
name = "github.com/beorn7/perks" name = "github.com/beorn7/perks"
packages = ["quantile"] packages = ["quantile"]
pruneopts = "" pruneopts = ""
revision = "4c0e84591b9aa9e6dcfdf3e020114cd81f89d5f9" revision = "4c0e84591b9aa9e6dcfdf3e020114cd81f89d5f9"
[[projects]] [[projects]]
digest = "1:d9d9c71f9776ef8f15b5c0a20246d5303071294743863ac3f4dde056f8c7b40a" digest = "1:85fd00554a6ed5b33687684b76635d532c74141508b5bce2843d85e8a3c9dc91"
name = "github.com/cloudflare/cloudflare-go" name = "github.com/cloudflare/cloudflare-go"
packages = ["."] packages = ["."]
pruneopts = "" pruneopts = ""
@ -136,29 +136,22 @@
version = "v0.7.4" version = "v0.7.4"
[[projects]] [[projects]]
digest = "1:31259dbcb4c073aace59b951f5b471b3d5dbc4051b4a9d7e000f4392e143977e" digest = "1:eaeede87b418b97f9dee473f8940fd9b65ca5cdac0503350c7c8f8965ea3cf4d"
name = "github.com/coreos/etcd" name = "github.com/coreos/etcd"
packages = [ packages = [
"client", "auth/authpb",
"pkg/pathutil", "clientv3",
"pkg/srv", "etcdserver/api/v3rpc/rpctypes",
"etcdserver/etcdserverpb",
"mvcc/mvccpb",
"pkg/types", "pkg/types",
"version",
] ]
pruneopts = "" pruneopts = ""
revision = "1b3ac99e8a431b381e633802cc42fe70e663baf5" revision = "1b3ac99e8a431b381e633802cc42fe70e663baf5"
version = "v3.2.15" version = "v3.2.15"
[[projects]] [[projects]]
digest = "1:3c3f68ebab415344aef64363d23471e953a4715645115604aaf57923ae904f5e" digest = "1:56c130d885a4aacae1dd9c7b71cfe39912c7ebc1ff7d2b46083c8812996dc43b"
name = "github.com/coreos/go-semver"
packages = ["semver"]
pruneopts = ""
revision = "8ab6407b697782a06568d4b7f1db25550ec2e4c6"
version = "v0.2.0"
[[projects]]
digest = "1:0a39ec8bf5629610a4bc7873a92039ee509246da3cef1a0ea60f1ed7e5f9cea5"
name = "github.com/davecgh/go-spew" name = "github.com/davecgh/go-spew"
packages = ["spew"] packages = ["spew"]
pruneopts = "" pruneopts = ""
@ -167,7 +160,7 @@
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:64ee6871ef691c663f910e29bc2f7c10c8c342b06665920f1138b6aa8b11cb5a" digest = "1:dc166ce7345c060c2153561130d6d49ac580c804226ac675e368d533b36eb169"
name = "github.com/denverdino/aliyungo" name = "github.com/denverdino/aliyungo"
packages = [ packages = [
"metadata", "metadata",
@ -177,7 +170,7 @@
revision = "69560d9530f5265ba00ffad2520d7ef01c2cddd4" revision = "69560d9530f5265ba00ffad2520d7ef01c2cddd4"
[[projects]] [[projects]]
digest = "1:2426da75f49e5b8507a6ed5d4c49b06b2ff795f4aec401c106b7db8fb2625cd7" digest = "1:6098222470fe0172157ce9bbef5d2200df4edde17ee649c5d6e48330e4afa4c6"
name = "github.com/dgrijalva/jwt-go" name = "github.com/dgrijalva/jwt-go"
packages = ["."] packages = ["."]
pruneopts = "" pruneopts = ""
@ -185,7 +178,7 @@
version = "v3.2.0" version = "v3.2.0"
[[projects]] [[projects]]
digest = "1:3da5806ef37ea163fee80ed179d40a5e013e671ccbe321a04c47c5aee3d5080a" digest = "1:32d1941b093bb945de75b0276348494be318d34f3df39c4413d61e002c800bc6"
name = "github.com/digitalocean/godo" name = "github.com/digitalocean/godo"
packages = [ packages = [
".", ".",
@ -196,7 +189,7 @@
version = "v1.1.1" version = "v1.1.1"
[[projects]] [[projects]]
digest = "1:ca3b228bf258217cff2070f4045e53729886c66a27bf9cce30dcbf8a575ea86a" digest = "1:5ffd39844bdd1259a6227d544f582c6686ce43c8c44399a46052fe3bd2bed93c"
name = "github.com/dnsimple/dnsimple-go" name = "github.com/dnsimple/dnsimple-go"
packages = ["dnsimple"] packages = ["dnsimple"]
pruneopts = "" pruneopts = ""
@ -204,7 +197,7 @@
version = "v0.14.0" version = "v0.14.0"
[[projects]] [[projects]]
digest = "1:bfce2cc5b829073f93962e742275d45913948e22d182fbc5464104da1c5f2f89" digest = "1:e17d18b233f506404061c27ac4a08624dad38dcd0d49f9cfdae67a7772a4fb8c"
name = "github.com/exoscale/egoscale" name = "github.com/exoscale/egoscale"
packages = ["."] packages = ["."]
pruneopts = "" pruneopts = ""
@ -213,7 +206,7 @@
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:bc12846e4bae094e01a33ef98cad0a1afa35da37090e5126513be6f747e074ab" digest = "1:ae7fb2062735e966ab69d14d2a091f3778b0d676dc8d1f01d092bcb0fb8ed45b"
name = "github.com/ffledgling/pdns-go" name = "github.com/ffledgling/pdns-go"
packages = ["."] packages = ["."]
pruneopts = "" pruneopts = ""
@ -228,7 +221,7 @@
version = "v1.0.0" version = "v1.0.0"
[[projects]] [[projects]]
digest = "1:bbc763f3c703dc3c6a99a22c1318760099b52bc00a47a36dc4462e88eee7846b" digest = "1:a00483fe4106b86fb1187a92b5cf6915c85f294ed4c129ccbe7cb1f1a06abd46"
name = "github.com/go-ini/ini" name = "github.com/go-ini/ini"
packages = ["."] packages = ["."]
pruneopts = "" pruneopts = ""
@ -236,7 +229,7 @@
version = "v1.32.0" version = "v1.32.0"
[[projects]] [[projects]]
digest = "1:cdeb6a9eb9f2356b2987c401d013d41e018b819ee1e8d5a1b32a5b714e53c392" digest = "1:8e67153fc0a9fb0d6c9707e36cf80e217a012364307b222eb4ba6828f7e881e6"
name = "github.com/go-resty/resty" name = "github.com/go-resty/resty"
packages = ["."] packages = ["."]
pruneopts = "" pruneopts = ""
@ -244,7 +237,7 @@
version = "v1.8.0" version = "v1.8.0"
[[projects]] [[projects]]
digest = "1:d7b2f8af8341e15d0239dab17cb49fbf4f01029ecf2d3b5924aa53d95c5a452d" digest = "1:54d5c6a784a9de9c836fc070d51d0689c3e99ee6d24dba8a36f0762039dae830"
name = "github.com/gogo/googleapis" name = "github.com/gogo/googleapis"
packages = ["google/rpc"] packages = ["google/rpc"]
pruneopts = "" pruneopts = ""
@ -252,7 +245,7 @@
version = "v1.1.0" version = "v1.1.0"
[[projects]] [[projects]]
digest = "1:673df1d02ca0c6f51458fe94bbb6fae0b05e54084a31db2288f1c4321255c2da" digest = "1:6e73003ecd35f4487a5e88270d3ca0a81bc80dc88053ac7e4dcfec5fba30d918"
name = "github.com/gogo/protobuf" name = "github.com/gogo/protobuf"
packages = [ packages = [
"gogoproto", "gogoproto",
@ -276,11 +269,12 @@
source = "github.com/kubermatic/glog-logrus" source = "github.com/kubermatic/glog-logrus"
[[projects]] [[projects]]
digest = "1:815d45503dceeca8ffecce0081d7edeae5e75b126107ef763d1c617154d72359" digest = "1:3dd078fda7500c341bc26cfbc6c6a34614f295a2457149fc1045cab767cbcf18"
name = "github.com/golang/protobuf" name = "github.com/golang/protobuf"
packages = [ packages = [
"jsonpb", "jsonpb",
"proto", "proto",
"protoc-gen-go/descriptor",
"ptypes", "ptypes",
"ptypes/any", "ptypes/any",
"ptypes/duration", "ptypes/duration",
@ -315,7 +309,7 @@
revision = "44d81051d367757e1c7c6a5a86423ece9afcf63c" revision = "44d81051d367757e1c7c6a5a86423ece9afcf63c"
[[projects]] [[projects]]
digest = "1:1962b5d00f5285d08504697049627d45ad876912894528d31cdc1c05cdc853f6" digest = "1:16b2837c8b3cf045fa2cdc82af0cf78b19582701394484ae76b2c3bc3c99ad73"
name = "github.com/googleapis/gnostic" name = "github.com/googleapis/gnostic"
packages = [ packages = [
"OpenAPIv2", "OpenAPIv2",
@ -328,7 +322,7 @@
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:815036d12757902f85888f3cb0440c2e00220dd4177e4c2bb048e03259db077a" digest = "1:54a44d48a24a104e078ef5f94d82f025a6be757e7c42b4370c621a3928d6ab7c"
name = "github.com/gophercloud/gophercloud" name = "github.com/gophercloud/gophercloud"
packages = [ packages = [
".", ".",
@ -362,7 +356,7 @@
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:8c4d156acec272201ffc4d1bdb9302de1c48314e0451eb38c70150cf11bdb33a" digest = "1:009a1928b8c096338b68b5822d838a72b4d8520715c1463614476359f3282ec8"
name = "github.com/gregjones/httpcache" name = "github.com/gregjones/httpcache"
packages = [ packages = [
".", ".",
@ -422,14 +416,14 @@
revision = "61dc5f9b0a655ebf43026f0d8a837ad1e28e4b96" revision = "61dc5f9b0a655ebf43026f0d8a837ad1e28e4b96"
[[projects]] [[projects]]
digest = "1:4f767a115bc8e08576f6d38ab73c376fc1b1cd3bb5041171c9e8668cc7739b52" digest = "1:6f49eae0c1e5dab1dafafee34b207aeb7a42303105960944828c2079b92fc88e"
name = "github.com/jmespath/go-jmespath" name = "github.com/jmespath/go-jmespath"
packages = ["."] packages = ["."]
pruneopts = "" pruneopts = ""
revision = "0b12d6b5" revision = "0b12d6b5"
[[projects]] [[projects]]
digest = "1:890dd7615573f096655600bbe7beb2f532a437f6d8ef237831894301fca31f23" digest = "1:53ac4e911e12dde0ab68655e2006449d207a5a681f084974da2b06e5dbeaca72"
name = "github.com/json-iterator/go" name = "github.com/json-iterator/go"
packages = ["."] packages = ["."]
pruneopts = "" pruneopts = ""
@ -437,14 +431,14 @@
version = "1.1.4" version = "1.1.4"
[[projects]] [[projects]]
digest = "1:def40684a573560241c8344da452fa3574dfc2c7da525903992a3790d2262625" digest = "1:1c88ec29544b281964ed7a9a365b2802a523cd06c50cdee87eb3eec89cd864f4"
name = "github.com/kubernetes/repo-infra" name = "github.com/kubernetes/repo-infra"
packages = ["verify/boilerplate/test"] packages = ["verify/boilerplate/test"]
pruneopts = "" pruneopts = ""
revision = "c2f9667a4c29e70a39b0e89db2d4f0cab907dbee" revision = "c2f9667a4c29e70a39b0e89db2d4f0cab907dbee"
[[projects]] [[projects]]
digest = "1:c3aa5f9d5119ca1cfdaa41a5084e3deceef0460eef3e6c71b58fa50e500f01a0" digest = "1:7c23a751ce2f84663fa411acb87eae0da2d09c39a8e99b08bd8f65fae75d8928"
name = "github.com/linki/instrumented_http" name = "github.com/linki/instrumented_http"
packages = ["."] packages = ["."]
pruneopts = "" pruneopts = ""
@ -452,7 +446,7 @@
version = "v0.2.0" version = "v0.2.0"
[[projects]] [[projects]]
digest = "1:93d29291d0c37678592d77ee847031aec2ce1631f3ce4cf975b77216e8bd4a01" digest = "1:1c41354ef11c9dbae2fe1ceee8369fcb2634977ba07e701e19ea53e8742c5420"
name = "github.com/linode/linodego" name = "github.com/linode/linodego"
packages = ["."] packages = ["."]
pruneopts = "" pruneopts = ""
@ -461,14 +455,14 @@
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:49a8b01a6cd6558d504b65608214ca40a78000e1b343ed0da5c6a9ccd83d6d30" digest = "1:63722a4b1e1717be7b98fc686e0b30d5e7f734b9e93d7dee86293b6deab7ea28"
name = "github.com/matttproud/golang_protobuf_extensions" name = "github.com/matttproud/golang_protobuf_extensions"
packages = ["pbutil"] packages = ["pbutil"]
pruneopts = "" pruneopts = ""
revision = "c12348ce28de40eed0136aa2b644d0ee0650e56c" revision = "c12348ce28de40eed0136aa2b644d0ee0650e56c"
[[projects]] [[projects]]
digest = "1:f0bad0fece0fb73c6ea249c18d8e80ffbe86be0457715b04463068f04686cf39" digest = "1:4c8d8358c45ba11ab7bb15df749d4df8664ff1582daead28bae58cf8cbe49890"
name = "github.com/miekg/dns" name = "github.com/miekg/dns"
packages = ["."] packages = ["."]
pruneopts = "" pruneopts = ""
@ -500,7 +494,7 @@
version = "v2.1" version = "v2.1"
[[projects]] [[projects]]
digest = "1:7aef6d4ad1b4a613d66ed554010c552a249e9afabcb079f54528a298474549cc" digest = "1:d8b5d0ecca348c835914a1ed8589f17a6a7f309befab7327b0470324531f7ac4"
name = "github.com/nesv/go-dynect" name = "github.com/nesv/go-dynect"
packages = ["dynect"] packages = ["dynect"]
pruneopts = "" pruneopts = ""
@ -508,7 +502,7 @@
version = "v0.6.0" version = "v0.6.0"
[[projects]] [[projects]]
digest = "1:2062e45c462d0327f680340dce46fe11ae2d34bf802e15e397cb1d6c4d159b39" digest = "1:70df8e71a953626770223d4982801fa73e7e99cbfcca068b95127f72af9b9edd"
name = "github.com/oracle/oci-go-sdk" name = "github.com/oracle/oci-go-sdk"
packages = [ packages = [
"common", "common",
@ -520,14 +514,14 @@
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:b7be9a944fe102bf466420fa8a064534dd12547a0482f5b684d228425b559b56" digest = "1:c24598ffeadd2762552269271b3b1510df2d83ee6696c1e543a0ff653af494bc"
name = "github.com/petar/GoLLRB" name = "github.com/petar/GoLLRB"
packages = ["llrb"] packages = ["llrb"]
pruneopts = "" pruneopts = ""
revision = "53be0d36a84c2a886ca057d34b6aa4468df9ccb4" revision = "53be0d36a84c2a886ca057d34b6aa4468df9ccb4"
[[projects]] [[projects]]
digest = "1:6db21ad64a13fe79220e47fcc895e13b8da923676a3a024f98210fca57a10d9a" digest = "1:b46305723171710475f2dd37547edd57b67b9de9f2a6267cafdd98331fd6897f"
name = "github.com/peterbourgon/diskv" name = "github.com/peterbourgon/diskv"
packages = ["."] packages = ["."]
pruneopts = "" pruneopts = ""
@ -550,7 +544,7 @@
version = "v1.0.0" version = "v1.0.0"
[[projects]] [[projects]]
digest = "1:7e88bda1bec34ddf3c8aded1326c652793069a673b0f751484953e7d65a2386c" digest = "1:2f69dc6b2685b31a1a410ef697410aa8a669704fb201d45dbd8c1911728afa75"
name = "github.com/prometheus/client_golang" name = "github.com/prometheus/client_golang"
packages = [ packages = [
"prometheus", "prometheus",
@ -562,7 +556,7 @@
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:83bf37d060fca77e959fe5ceee81e58bbd1b01836f4addc70043a948e9912547" digest = "1:60aca47f4eeeb972f1b9da7e7db51dee15ff6c59f7b401c1588b8e6771ba15ef"
name = "github.com/prometheus/client_model" name = "github.com/prometheus/client_model"
packages = ["go"] packages = ["go"]
pruneopts = "" pruneopts = ""
@ -570,7 +564,7 @@
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:7221d79e41a24b2245d06f331d0825b479a9acd0bd05a8353806c7bf38395795" digest = "1:e3aa5178be4fc4ae8cdb37d11c02f7490c00450a9f419e6aa84d02d3b47e90d2"
name = "github.com/prometheus/common" name = "github.com/prometheus/common"
packages = [ packages = [
"expfmt", "expfmt",
@ -581,7 +575,7 @@
revision = "2e54d0b93cba2fd133edc32211dcc32c06ef72ca" revision = "2e54d0b93cba2fd133edc32211dcc32c06ef72ca"
[[projects]] [[projects]]
digest = "1:91345f4cce04248cf4998c4f70a82579c1468101767636acf5af2e1556904933" digest = "1:a6a85fc81f2a06ccac3d45005523afbeee45138d781d4f3cb7ad9889d5c65aab"
name = "github.com/prometheus/procfs" name = "github.com/prometheus/procfs"
packages = [ packages = [
".", ".",
@ -599,7 +593,7 @@
version = "v1.2.0" version = "v1.2.0"
[[projects]] [[projects]]
digest = "1:75e2c10fd48881dc9400b7b70281270923e01c44f1f5cb4bbc5ba8cac8ca3026" digest = "1:3ac248add5bb40a3c631c5334adcd09aa72d15af2768a5bc0274084ea7b2e5ba"
name = "github.com/sirupsen/logrus" name = "github.com/sirupsen/logrus"
packages = ["."] packages = ["."]
pruneopts = "" pruneopts = ""
@ -607,7 +601,7 @@
version = "v1.0.3" version = "v1.0.3"
[[projects]] [[projects]]
digest = "1:39c598f67d5d68846c05832bb351e897091edcbee4689c57d3697f68f25f928d" digest = "1:a1403cc8a94b8d7956ee5e9694badef0e7b051af289caad1cf668331e3ffa4f6"
name = "github.com/spf13/cobra" name = "github.com/spf13/cobra"
packages = ["."] packages = ["."]
pruneopts = "" pruneopts = ""
@ -623,7 +617,7 @@
version = "v1.0.2" version = "v1.0.2"
[[projects]] [[projects]]
digest = "1:ba8fed52de60135b7efd5d832b997fb5b10fa09f227fa385174faa69f4219e4e" digest = "1:306417ea2f31ea733df356a2b895de63776b6a5107085b33458e5cd6eb1d584d"
name = "github.com/stretchr/objx" name = "github.com/stretchr/objx"
packages = ["."] packages = ["."]
pruneopts = "" pruneopts = ""
@ -631,7 +625,7 @@
version = "v0.1" version = "v0.1"
[[projects]] [[projects]]
digest = "1:a70d585d45f695f2e8e6782569bdf181419667a35e6035ceb086706b495aa21a" digest = "1:a30066593578732a356dc7e5d7f78d69184ca65aeeff5939241a3ab10559bb06"
name = "github.com/stretchr/testify" name = "github.com/stretchr/testify"
packages = [ packages = [
"assert", "assert",
@ -652,14 +646,7 @@
revision = "ac974c61c2f990f4115b119354b5e0b47550e888" revision = "ac974c61c2f990f4115b119354b5e0b47550e888"
[[projects]] [[projects]]
digest = "1:f98e0b7c7bd110a49d8bb56c9eefcef4f547f5d789025d3bfe9bd6b83125221b" digest = "1:74f86c458e82e1c4efbab95233e0cf51b7cc02dc03193be9f62cd81224e10401"
name = "github.com/ugorji/go"
packages = ["codec"]
pruneopts = ""
revision = "ded73eae5db7e7a0ef6f55aace87a2873c5d2b74"
[[projects]]
digest = "1:5e30725e7522642910b34208061b21bb0cd77b8ce115c3133a1431c52054e004"
name = "go.uber.org/atomic" name = "go.uber.org/atomic"
packages = ["."] packages = ["."]
pruneopts = "" pruneopts = ""
@ -675,7 +662,7 @@
version = "v1.1.0" version = "v1.1.0"
[[projects]] [[projects]]
digest = "1:2fabb14a874994210af33633091bd0eb070b50aa527767abaac1b5483db03d75" digest = "1:246f378f80fba6fcf0f191c486b6613265abd2bc0f2fa55a36b928c67352021e"
name = "go.uber.org/zap" name = "go.uber.org/zap"
packages = [ packages = [
".", ".",
@ -692,7 +679,7 @@
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:16db3d6f4f8bbe4b7b42cb8808e68457fea4bd7aea410b77c8c9a6dc26253a60" digest = "1:b2d8b39397ca07929a3de3a3fd2b6ca4c8d48e9cadaa7cf2b083e27fd9e78107"
name = "golang.org/x/crypto" name = "golang.org/x/crypto"
packages = [ packages = [
"ed25519", "ed25519",
@ -703,7 +690,7 @@
revision = "0709b304e793a5edb4a2c0145f281ecdc20838a4" revision = "0709b304e793a5edb4a2c0145f281ecdc20838a4"
[[projects]] [[projects]]
digest = "1:02feed0dbc44ce5bef5670a6e5ac9c2c4e3b879575a9d074199b487af1b7c4f9" digest = "1:782723d6fc27d202f1943219d68d58b3f6bcab6212c85294b1ddd8b586b1d356"
name = "golang.org/x/net" name = "golang.org/x/net"
packages = [ packages = [
"bpf", "bpf",
@ -725,7 +712,7 @@
revision = "161cd47e91fd58ac17490ef4d742dc98bb4cf60e" revision = "161cd47e91fd58ac17490ef4d742dc98bb4cf60e"
[[projects]] [[projects]]
digest = "1:2fef2e19e90f29efd775d58d66b5e100139fedbe24cf749f1c085c0a5aee86d3" digest = "1:dad5a319c4710358be1f2bf68f9fb7f90a71d7c641221b79801d5667b28f19e3"
name = "golang.org/x/oauth2" name = "golang.org/x/oauth2"
packages = [ packages = [
".", ".",
@ -738,7 +725,7 @@
revision = "3c3a985cb79f52a3190fbc056984415ca6763d01" revision = "3c3a985cb79f52a3190fbc056984415ca6763d01"
[[projects]] [[projects]]
digest = "1:d4315e749759007a597c9ad09eef29112bea98030da19ed29c33959ad6744130" digest = "1:39d88a855976e160babdd254802e1c2ae75ed380328c39742b57928852da6207"
name = "golang.org/x/sys" name = "golang.org/x/sys"
packages = [ packages = [
"unix", "unix",
@ -748,7 +735,7 @@
revision = "13d03a9a82fba647c21a0ef8fba44a795d0f0835" revision = "13d03a9a82fba647c21a0ef8fba44a795d0f0835"
[[projects]] [[projects]]
digest = "1:af9bfca4298ef7502c52b1459df274eed401a4f5498b900e9a92d28d3d87ac5a" digest = "1:5acd3512b047305d49e8763eef7ba423901e85d5dd2fd1e71778a0ea8de10bd4"
name = "golang.org/x/text" name = "golang.org/x/text"
packages = [ packages = [
"collate", "collate",
@ -779,7 +766,7 @@
revision = "fbb02b2291d28baffd63558aa44b4b56f178d650" revision = "fbb02b2291d28baffd63558aa44b4b56f178d650"
[[projects]] [[projects]]
digest = "1:ab84306e7e74b9f01b9f1480d46cca9325f8c512567a0e7b8888d04ff627a5ba" digest = "1:2ad38d79865e33dde6157b7048debd6e7d47e0709df7b5e11bb888168e316908"
name = "google.golang.org/api" name = "google.golang.org/api"
packages = [ packages = [
"dns/v1", "dns/v1",
@ -791,7 +778,7 @@
revision = "a0ff90edab763c86aa88f2b1eb4f3301b82f6336" revision = "a0ff90edab763c86aa88f2b1eb4f3301b82f6336"
[[projects]] [[projects]]
digest = "1:0b45fac4876cbd496ed7b95406b05c8c1eba559b43c82f2dda1b0e1bbe6cd1b6" digest = "1:41e2b7e287117f6136f75286d48072ecf781ba54823ffeb2098e152e7dc45ef6"
name = "google.golang.org/appengine" name = "google.golang.org/appengine"
packages = [ packages = [
".", ".",
@ -810,14 +797,17 @@
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:7040eaf95eb09f6f69e1415074049a9a66236d59d8767f2d17b759b916f79fb1" digest = "1:e43f1cb3f488a0c2be85939c2a594636f60b442a12a196c778bd2d6c9aca3df7"
name = "google.golang.org/genproto" name = "google.golang.org/genproto"
packages = ["googleapis/rpc/status"] packages = [
"googleapis/api/annotations",
"googleapis/rpc/status",
]
pruneopts = "" pruneopts = ""
revision = "11092d34479b07829b72e10713b159248caf5dad" revision = "11092d34479b07829b72e10713b159248caf5dad"
[[projects]] [[projects]]
digest = "1:cb1330030248de97a11d9f9664f3944fce0df947e5ed94dbbd9cb6e77068bd46" digest = "1:ca75b3775a5d4e5d1fb48f57ef0865b4aaa8b3f00e6b52be68db991c4594e0a7"
name = "google.golang.org/grpc" name = "google.golang.org/grpc"
packages = [ packages = [
".", ".",
@ -830,6 +820,7 @@
"encoding", "encoding",
"encoding/proto", "encoding/proto",
"grpclog", "grpclog",
"health/grpc_health_v1",
"internal", "internal",
"internal/backoff", "internal/backoff",
"internal/channelz", "internal/channelz",
@ -869,7 +860,7 @@
[[projects]] [[projects]]
branch = "release-1.0" branch = "release-1.0"
digest = "1:159d72863d2fdc7f8a7bf5178554bad51c45b003ba27021c3481f84b3cc8e155" digest = "1:bc43af6616d8ca12a7b8e806874387f0f1e181c08f547e9cd77f6cbac8cefd86"
name = "istio.io/api" name = "istio.io/api"
packages = [ packages = [
"authentication/v1alpha1", "authentication/v1alpha1",
@ -883,7 +874,7 @@
revision = "76349c53b87f03f1e610b3aa3843dba3c38138d7" revision = "76349c53b87f03f1e610b3aa3843dba3c38138d7"
[[projects]] [[projects]]
digest = "1:2e63e5a0a6abb75c20ea575ee82a72c117a811ab8472ed34a48a09ba25a0a7d7" digest = "1:7eb25280e1f610470bb0c43ab6c91573cfc78672a58542106b9b71705581429a"
name = "istio.io/istio" name = "istio.io/istio"
packages = [ packages = [
"pilot/pkg/config/kube/crd", "pilot/pkg/config/kube/crd",
@ -899,7 +890,7 @@
version = "1.0.1" version = "1.0.1"
[[projects]] [[projects]]
digest = "1:d42e6aef075bfc20da9c3991adfd8b09e3e158ac619028e15271677b705be5f0" digest = "1:f420c8548c93242d8e5dcfa5b34e0243883b4e660f65076e869daafac877144d"
name = "k8s.io/api" name = "k8s.io/api"
packages = [ packages = [
"admissionregistration/v1alpha1", "admissionregistration/v1alpha1",
@ -937,7 +928,7 @@
version = "kubernetes-1.11.0" version = "kubernetes-1.11.0"
[[projects]] [[projects]]
digest = "1:a14992570e0e2b0291594f505d2b2ed1a6ba4482d4166ace9714c2ba8cbfe252" digest = "1:66d1421ecff35bc48ee0b11a3f891f3af6f775ed6bb1d3e0deeaba221bf42490"
name = "k8s.io/apiextensions-apiserver" name = "k8s.io/apiextensions-apiserver"
packages = [ packages = [
"pkg/apis/apiextensions", "pkg/apis/apiextensions",
@ -951,7 +942,7 @@
version = "kubernetes-1.10.4" version = "kubernetes-1.10.4"
[[projects]] [[projects]]
digest = "1:a855f74be59f83ed0950a9a2b70d8c8af01fb5782d060c7dec67ae39033f30dc" digest = "1:b6b2fb7b4da1ac973b64534ace2299a02504f16bc7820cb48edb8ca4077183e1"
name = "k8s.io/apimachinery" name = "k8s.io/apimachinery"
packages = [ packages = [
"pkg/api/errors", "pkg/api/errors",
@ -1001,7 +992,7 @@
version = "kubernetes-1.11.0" version = "kubernetes-1.11.0"
[[projects]] [[projects]]
digest = "1:3c4611c2b28fdc62391698bba7f212050f0f9ed75f3648f37ec3bcf8a83bf96d" digest = "1:d04779a8de7d5465e0463bd986506348de5e89677c74777f695d3145a7a8d15e"
name = "k8s.io/client-go" name = "k8s.io/client-go"
packages = [ packages = [
"discovery", "discovery",
@ -1107,7 +1098,7 @@
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:d93d8bcb5f04d6b59eafdb9fa1a80f187d2542611670bfabc0ea8e031ab874a2" digest = "1:526095379da1098c3f191a0008cc59c9bf9927492e63da7689e5de424219c162"
name = "k8s.io/kube-openapi" name = "k8s.io/kube-openapi"
packages = ["pkg/util/proto"] packages = ["pkg/util/proto"]
pruneopts = "" pruneopts = ""
@ -1134,7 +1125,7 @@
"github.com/aws/aws-sdk-go/service/route53", "github.com/aws/aws-sdk-go/service/route53",
"github.com/aws/aws-sdk-go/service/servicediscovery", "github.com/aws/aws-sdk-go/service/servicediscovery",
"github.com/cloudflare/cloudflare-go", "github.com/cloudflare/cloudflare-go",
"github.com/coreos/etcd/client", "github.com/coreos/etcd/clientv3",
"github.com/denverdino/aliyungo/metadata", "github.com/denverdino/aliyungo/metadata",
"github.com/digitalocean/godo", "github.com/digitalocean/godo",
"github.com/digitalocean/godo/context", "github.com/digitalocean/godo/context",
@ -1173,6 +1164,7 @@
"istio.io/istio/pilot/pkg/model", "istio.io/istio/pilot/pkg/model",
"k8s.io/api/core/v1", "k8s.io/api/core/v1",
"k8s.io/api/extensions/v1beta1", "k8s.io/api/extensions/v1beta1",
"k8s.io/apimachinery/pkg/api/errors",
"k8s.io/apimachinery/pkg/apis/meta/v1", "k8s.io/apimachinery/pkg/apis/meta/v1",
"k8s.io/apimachinery/pkg/labels", "k8s.io/apimachinery/pkg/labels",
"k8s.io/apimachinery/pkg/runtime", "k8s.io/apimachinery/pkg/runtime",

View File

@ -24,10 +24,6 @@ ignored = ["github.com/kubernetes/repo-infra/kazel"]
name = "github.com/cloudflare/cloudflare-go" name = "github.com/cloudflare/cloudflare-go"
version = "0.7.3" version = "0.7.3"
[[constraint]]
name = "github.com/coreos/etcd"
version = "~3.2.15"
[[constraint]] [[constraint]]
name = "github.com/digitalocean/godo" name = "github.com/digitalocean/godo"
version = "~1.1.0" version = "~1.1.0"

View File

@ -91,10 +91,12 @@ func TestRunOnce(t *testing.T) {
source.On("Endpoints").Return([]*endpoint.Endpoint{ source.On("Endpoints").Return([]*endpoint.Endpoint{
{ {
DNSName: "create-record", DNSName: "create-record",
RecordType: endpoint.RecordTypeA,
Targets: endpoint.Targets{"1.2.3.4"}, Targets: endpoint.Targets{"1.2.3.4"},
}, },
{ {
DNSName: "update-record", DNSName: "update-record",
RecordType: endpoint.RecordTypeA,
Targets: endpoint.Targets{"8.8.4.4"}, Targets: endpoint.Targets{"8.8.4.4"},
}, },
}, nil) }, nil)
@ -104,25 +106,27 @@ func TestRunOnce(t *testing.T) {
[]*endpoint.Endpoint{ []*endpoint.Endpoint{
{ {
DNSName: "update-record", DNSName: "update-record",
RecordType: endpoint.RecordTypeA,
Targets: endpoint.Targets{"8.8.8.8"}, Targets: endpoint.Targets{"8.8.8.8"},
}, },
{ {
DNSName: "delete-record", DNSName: "delete-record",
RecordType: endpoint.RecordTypeA,
Targets: endpoint.Targets{"4.3.2.1"}, Targets: endpoint.Targets{"4.3.2.1"},
}, },
}, },
&plan.Changes{ &plan.Changes{
Create: []*endpoint.Endpoint{ 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{ 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{ 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{ 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"}},
}, },
}, },
) )

View File

@ -69,6 +69,7 @@ Regarding Ingress, we'll support:
* Google's Ingress Controller on GKE that integrates with their Layer 7 load balancers (GLBC) * 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 * 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) * 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? ### Are other Ingress Controllers supported?

View File

@ -286,7 +286,7 @@ spec:
After roughly two minutes check that a corresponding DNS record for your service was created. After roughly two minutes check that a corresponding DNS record for your service was created.
```console ```console
$ aliyun aliyun alidns DescribeDomainRecords --DomainName=external-dns-test.com $ aliyun alidns DescribeDomainRecords --DomainName=external-dns-test.com
{ {
"PageNumber": 1, "PageNumber": 1,
"TotalCount": 1, "TotalCount": 1,

View File

@ -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 `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) ## Verify ExternalDNS works (Ingress example)

View File

@ -3,7 +3,7 @@
This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster on Azure. 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 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 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 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. the `--azure-config-file` option when starting ExternalDNS.
### Azure Container Services ### Use provisioned VM configuration file
When your Kubernetes cluster is created by ACS, a file named `/etc/kubernetes/azure.json` is created to store When running within Azure (ACS or AKS), the agent and master VMs are already provisioned with the configuration file at `/etc/kubernetes/azure.json`.
the Azure credentials for API access. Kubernetes uses this file for the Azure cloud provider.
For ExternalDNS to access the Azure API, it also needs access to this file. However, we will be deploying ExternalDNS inside of 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).
the Kubernetes cluster so we will need to use a Kubernetes secret.
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: 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 #### 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. 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 "password": "password", <-- aadClientSecret value
"tenant": "AzureAD Tenant Id" <-- tenantId value "tenant": "AzureAD Tenant Id" <-- tenantId value
} }
... ```
``` #### Azure Managed Service Identity (MSI)
### 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: 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", "tenantId": "01234abc-de56-ff78-abc1-234567890def",
"subscriptionId": "Id", "subscriptionId": "01234abc-de56-ff78-abc1-234567890def",
"aadClientId": "Service Principal AppId",
"aadClientSecret": "Service Principal Password",
"resourceGroup": "MyDnsResourceGroup", "resourceGroup": "MyDnsResourceGroup",
} "useManagedIdentityExtension": true
```
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,
} }
``` ```
@ -170,7 +198,7 @@ spec:
secretName: azure-config-file secretName: azure-config-file
``` ```
### Manifest (for clusters with RBAC enabled) ### Manifest (for clusters with RBAC enabled, cluster access)
```yaml ```yaml
apiVersion: v1 apiVersion: v1
kind: ServiceAccount kind: ServiceAccount
@ -240,6 +268,76 @@ spec:
secretName: azure-config-file 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: Create the deployment for ExternalDNS:
``` ```

View File

@ -22,6 +22,7 @@ auth:
region: us-phoenix-1 region: us-phoenix-1
tenancy: ocid1.tenancy.oc1... tenancy: ocid1.tenancy.oc1...
user: ocid1.user.oc1... user: ocid1.user.oc1...
key: |
-----BEGIN RSA PRIVATE KEY----- -----BEGIN RSA PRIVATE KEY-----
-----END RSA PRIVATE KEY----- -----END RSA PRIVATE KEY-----
fingerprint: af:81:71:8e... fingerprint: af:81:71:8e...

View File

@ -15,8 +15,7 @@ anyway.
The PDNS provider currently does not support: The PDNS provider currently does not support:
1. Dry running a configuration is not supported. * Dry running a configuration is not supported
2. The `--domain-filter` flag is not supported.
## Deployment ## Deployment
@ -47,10 +46,18 @@ spec:
- --pdns-server={{ pdns-api-url }} - --pdns-server={{ pdns-api-url }}
- --pdns-api-key={{ pdns-http-api-key }} - --pdns-api-key={{ pdns-http-api-key }}
- --txt-owner-id={{ owner-id-for-this-external-dns }} - --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 - --log-level=debug
- --interval=30s - --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 ## RBAC
If your cluster is RBAC enabled, you also need to setup the following, before you can run external-dns: If your cluster is RBAC enabled, you also need to setup the following, before you can run external-dns:

View File

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

View File

@ -135,10 +135,10 @@ func (t planTable) getDeletes() (deleteList []*endpoint.Endpoint) {
func (p *Plan) Calculate() *Plan { func (p *Plan) Calculate() *Plan {
t := newPlanTable() t := newPlanTable()
for _, current := range p.Current { for _, current := range filterRecordsForPlan(p.Current) {
t.addCurrent(current) t.addCurrent(current)
} }
for _, desired := range p.Desired { for _, desired := range filterRecordsForPlan(p.Desired) {
t.addCandidate(desired) t.addCandidate(desired)
} }
@ -180,6 +180,30 @@ func shouldUpdateTTL(desired, current *endpoint.Endpoint) bool {
return desired.RecordTTL != current.RecordTTL 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 // sanitizeDNSName checks if the DNS name is correct
// for now it only removes space and lower case // for now it only removes space and lower case
func sanitizeDNSName(dnsName string) string { func sanitizeDNSName(dnsName string) string {

View File

@ -29,6 +29,7 @@ type PlanTestSuite struct {
suite.Suite suite.Suite
fooV1Cname *endpoint.Endpoint fooV1Cname *endpoint.Endpoint
fooV2Cname *endpoint.Endpoint fooV2Cname *endpoint.Endpoint
fooV2TXT *endpoint.Endpoint
fooV2CnameNoLabel *endpoint.Endpoint fooV2CnameNoLabel *endpoint.Endpoint
fooV3CnameSameResource *endpoint.Endpoint fooV3CnameSameResource *endpoint.Endpoint
fooA5 *endpoint.Endpoint fooA5 *endpoint.Endpoint
@ -65,6 +66,10 @@ func (suite *PlanTestSuite) SetupTest() {
endpoint.ResourceLabelKey: "ingress/default/foo-v2", endpoint.ResourceLabelKey: "ingress/default/foo-v2",
}, },
} }
suite.fooV2TXT = &endpoint.Endpoint{
DNSName: "foo",
RecordType: "TXT",
}
suite.fooV2CnameNoLabel = &endpoint.Endpoint{ suite.fooV2CnameNoLabel = &endpoint.Endpoint{
DNSName: "foo", DNSName: "foo",
Targets: endpoint.Targets{"v2"}, Targets: endpoint.Targets{"v2"},
@ -262,6 +267,27 @@ func (suite *PlanTestSuite) TestDifferentTypes() {
validateEntries(suite.T(), changes.Delete, expectedDelete) 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() { func (suite *PlanTestSuite) TestRemoveEndpoint() {
current := []*endpoint.Endpoint{suite.fooV1Cname, suite.bar192A} current := []*endpoint.Endpoint{suite.fooV1Cname, suite.bar192A}
desired := []*endpoint.Endpoint{suite.fooV1Cname} desired := []*endpoint.Endpoint{suite.fooV1Cname}

View File

@ -684,18 +684,24 @@ func (p *AlibabaCloudProvider) splitDNSName(endpoint *endpoint.Endpoint) (rr str
} }
if !found { if !found {
idx := strings.Index(name, ".") parts := strings.Split(name, ".")
if idx >= 0 { if len(parts) < 2 {
rr = name[0:idx]
domain = name[idx+1:]
} else {
rr = name rr = name
domain = "" 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 == "" { if rr == "" {
rr = nullHostAlibabaCloud rr = nullHostAlibabaCloud
} }
return rr, domain return rr, domain
} }

View File

@ -404,6 +404,16 @@ func TestAlibabaCloudProvider_splitDNSName(t *testing.T) {
if rr != "@" || domain != "container-service.top" { if rr != "@" || domain != "container-service.top" {
t.Errorf("Failed to splitDNSName for %s: rr=%s, domain=%s", endpoint.DNSName, rr, domain) 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) { func TestAlibabaCloudProvider_TXTEndpoint(t *testing.T) {

View File

@ -193,7 +193,7 @@ func (p *AWSProvider) Zones() (map[string]*route53.HostedZone, error) {
// wildcardUnescape converts \\052.abc back to *.abc // 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 // 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 { func wildcardUnescape(s string) string {
if strings.HasPrefix(s, "\\052") { if strings.Contains(s, "\\052") {
s = strings.Replace(s, "\\052", "*", 1) s = strings.Replace(s, "\\052", "*", 1)
} }
return s 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) { if isAWSLoadBalancer(endpoint) {
evalTargetHealth := p.evaluateTargetHealth evalTargetHealth := p.evaluateTargetHealth
if _, ok := endpoint.ProviderSpecific[providerSpecificEvaluateTargetHealth]; ok { 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])), HostedZoneId: aws.String(canonicalHostedZone(endpoint.Targets[0])),
EvaluateTargetHealth: aws.Bool(evalTargetHealth), 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 { } else {
change.ResourceRecordSet.Type = aws.String(endpoint.RecordType) change.ResourceRecordSet.Type = aws.String(endpoint.RecordType)
if !endpoint.RecordTTL.IsConfigured() { if !endpoint.RecordTTL.IsConfigured() {
@ -529,6 +547,21 @@ func isAWSLoadBalancer(ep *endpoint.Endpoint) bool {
return false 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. // canonicalHostedZone returns the matching canonical zone for a given hostname.
func canonicalHostedZone(hostname string) string { func canonicalHostedZone(hostname string) string {
for suffix, zone := range canonicalHostedZones { for suffix, zone := range canonicalHostedZones {
@ -539,3 +572,11 @@ func canonicalHostedZone(hostname string) string {
return "" return ""
} }
// cleanZoneID removes the "/hostedzone/" prefix
func cleanZoneID(ID string) string {
if strings.HasPrefix(ID, "/hostedzone/") {
ID = strings.TrimPrefix(ID, "/hostedzone/")
}
return ID
}

View File

@ -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 // 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 { func wildcardEscape(s string) string {
if strings.HasPrefix(s, "*") { if strings.Contains(s, "*") {
s = strings.Replace(s, "*", "\\052", 1) s = strings.Replace(s, "*", "\\052", 1)
} }
return s 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("*.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.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("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() 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("*.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.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("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) { func TestAWSCanonicalHostedZone(t *testing.T) {
for _, tc := range []struct { for _, tc := range []struct {
hostname string hostname string
@ -929,10 +960,13 @@ func setupAWSRecords(t *testing.T, provider *AWSProvider, endpoints []*endpoint.
require.NoError(t, provider.CreateRecords(endpoints)) 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() records, err = provider.Records()
require.NoError(t, err) require.NoError(t, err)
validateEndpoints(t, records, endpoints)
} }
func listAWSRecords(t *testing.T, client Route53API, zone string) []*route53.ResourceRecordSet { func listAWSRecords(t *testing.T, client Route53API, zone string) []*route53.ResourceRecordSet {
@ -941,11 +975,8 @@ func listAWSRecords(t *testing.T, client Route53API, zone string) []*route53.Res
HostedZoneId: aws.String(zone), HostedZoneId: aws.String(zone),
}, func(resp *route53.ListResourceRecordSetsOutput, _ bool) bool { }, func(resp *route53.ListResourceRecordSetsOutput, _ bool) bool {
for _, recordSet := range resp.ResourceRecordSets { 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 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) { func newAWSProvider(t *testing.T, domainFilter DomainFilter, zoneIDFilter ZoneIDFilter, zoneTypeFilter ZoneTypeFilter, evaluateTargetHealth, dryRun bool, records []*endpoint.Endpoint) (*AWSProvider, *Route53APIStub) {
client := NewRoute53APIStub() client := NewRoute53APIStub()

View File

@ -48,7 +48,7 @@ func createMockZone(zone string, id string) dns.Zone {
} }
func (client *mockZonesClient) ListByResourceGroup(resourceGroupName string, top *int32) (dns.ZoneListResult, error) { 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 // of the Azure DNS service
return *client.mockZoneListResult, nil return *client.mockZoneListResult, nil
} }

View File

@ -17,7 +17,7 @@ limitations under the License.
package provider package provider
import ( import (
"container/list" "context"
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"encoding/json" "encoding/json"
@ -26,14 +26,12 @@ import (
"io/ioutil" "io/ioutil"
"math/rand" "math/rand"
"net" "net"
"net/http"
"os" "os"
"strings" "strings"
"time" "time"
etcd "github.com/coreos/etcd/client" etcdcv3 "github.com/coreos/etcd/clientv3"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"golang.org/x/net/context"
"github.com/kubernetes-incubator/external-dns/endpoint" "github.com/kubernetes-incubator/external-dns/endpoint"
"github.com/kubernetes-incubator/external-dns/plan" "github.com/kubernetes-incubator/external-dns/plan"
@ -43,8 +41,17 @@ func init() {
rand.Seed(time.Now().UnixNano()) rand.Seed(time.Now().UnixNano())
} }
// skyDNSClient is an interface to work with SkyDNS service records in etcd const (
type skyDNSClient interface { 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) GetServices(prefix string) ([]*Service, error)
SaveService(value *Service) error SaveService(value *Service) error
DeleteService(key string) error DeleteService(key string) error
@ -53,10 +60,10 @@ type skyDNSClient interface {
type coreDNSProvider struct { type coreDNSProvider struct {
dryRun bool dryRun bool
domainFilter DomainFilter domainFilter DomainFilter
client skyDNSClient client coreDNSClient
} }
// Service represents SkyDNS/CoreDNS etcd record // Service represents CoreDNS etcd record
type Service struct { type Service struct {
Host string `json:"host,omitempty"` Host string `json:"host,omitempty"`
Port int `json:"port,omitempty"` Port int `json:"port,omitempty"`
@ -83,52 +90,58 @@ type Service struct {
} }
type etcdClient 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) // GetService return all Service records stored in etcd stored anywhere under the given key (recursively)
func (c etcdClient) GetServices(prefix string) ([]*Service, error) { func (c etcdClient) GetServices(prefix string) ([]*Service, error) {
var result []*Service ctx, cancel := context.WithTimeout(c.ctx, etcdTimeout)
opts := &etcd.GetOptions{Recursive: true} defer cancel()
data, err := c.api.Get(context.Background(), prefix, opts)
path := prefix
r, err := c.client.Get(ctx, path, etcdcv3.WithPrefix())
if err != nil { if err != nil {
if etcd.IsKeyNotFound(err) {
return nil, nil
}
return nil, err return nil, err
} }
queue := list.New() var svcs []*Service
queue.PushFront(data.Node) bx := make(map[Service]bool)
for queueNode := queue.Front(); queueNode != nil; queueNode = queueNode.Next() { for _, n := range r.Kvs {
node := queueNode.Value.(*etcd.Node) svc := new(Service)
if node.Dir { if err := json.Unmarshal(n.Value, svc); err != nil {
for _, childNode := range node.Nodes { return nil, fmt.Errorf("%s: %s", n.Key, err.Error())
queue.PushBack(childNode)
} }
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 continue
} }
service := &Service{} bx[b] = true
err = json.Unmarshal([]byte(node.Value), service)
if err != nil { svc.Key = string(n.Key)
log.Error("Cannot parse JSON value ", node.Value) if svc.Priority == 0 {
continue svc.Priority = priority
} }
service.Key = node.Key svcs = append(svcs, svc)
result = append(result, service)
} }
return result, nil
return svcs, nil
} }
// SaveService persists service data into etcd // SaveService persists service data into etcd
func (c etcdClient) SaveService(service *Service) error { func (c etcdClient) SaveService(service *Service) error {
ctx, cancel := context.WithTimeout(c.ctx, etcdTimeout)
defer cancel()
value, err := json.Marshal(&service) value, err := json.Marshal(&service)
if err != nil { if err != nil {
return err 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 { if err != nil {
return err return err
} }
@ -137,9 +150,11 @@ func (c etcdClient) SaveService(service *Service) error {
// DeleteService deletes service record from etcd // DeleteService deletes service record from etcd
func (c etcdClient) DeleteService(key string) error { func (c etcdClient) DeleteService(key string) error {
_, err := c.api.Delete(context.Background(), key, nil) ctx, cancel := context.WithTimeout(c.ctx, etcdTimeout)
return err defer cancel()
_, err := c.client.Delete(ctx, key)
return err
} }
// loads TLS artifacts and builds tls.Clonfig object // loads TLS artifacts and builds tls.Clonfig object
@ -186,21 +201,8 @@ func loadRoots(caPath string) (*x509.CertPool, error) {
return roots, nil 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 // 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") etcdURLsStr := os.Getenv("ETCD_URLS")
if etcdURLsStr == "" { if etcdURLsStr == "" {
etcdURLsStr = "http://localhost:2379" etcdURLsStr = "http://localhost:2379"
@ -208,7 +210,7 @@ func getETCDConfig() (*etcd.Config, error) {
etcdURLs := strings.Split(etcdURLsStr, ",") etcdURLs := strings.Split(etcdURLsStr, ",")
firstURL := strings.ToLower(etcdURLs[0]) firstURL := strings.ToLower(etcdURLs[0])
if strings.HasPrefix(firstURL, "http://") { if strings.HasPrefix(firstURL, "http://") {
return &etcd.Config{Endpoints: etcdURLs}, nil return &etcdcv3.Config{Endpoints: etcdURLs}, nil
} else if strings.HasPrefix(firstURL, "https://") { } else if strings.HasPrefix(firstURL, "https://") {
caFile := os.Getenv("ETCD_CA_FILE") caFile := os.Getenv("ETCD_CA_FILE")
certFile := os.Getenv("ETCD_CERT_FILE") certFile := os.Getenv("ETCD_CERT_FILE")
@ -220,9 +222,9 @@ func getETCDConfig() (*etcd.Config, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &etcd.Config{ return &etcdcv3.Config{
Endpoints: etcdURLs, Endpoints: etcdURLs,
Transport: newHTTPSTransport(tlsConfig), TLS: tlsConfig,
}, nil }, nil
} else { } else {
return nil, errors.New("etcd URLs must start with either http:// or https://") 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 //newETCDClient is an etcd client constructor
func newETCDClient() (skyDNSClient, error) { func newETCDClient() (coreDNSClient, error) {
cfg, err := getETCDConfig() cfg, err := getETCDConfig()
if err != nil { if err != nil {
return nil, err return nil, err
} }
c, err := etcd.New(*cfg) c, err := etcdcv3.New(*cfg)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return etcdClient{etcd.NewKeysAPI(c)}, nil return etcdClient{c, context.Background()}, nil
} }
// NewCoreDNSProvider is a CoreDNS provider constructor // NewCoreDNSProvider is a CoreDNS provider constructor
@ -255,16 +257,16 @@ func NewCoreDNSProvider(domainFilter DomainFilter, dryRun bool) (Provider, error
}, nil }, 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 // 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) { func (p coreDNSProvider) Records() ([]*endpoint.Endpoint, error) {
var result []*endpoint.Endpoint var result []*endpoint.Endpoint
services, err := p.client.GetServices("/skydns") services, err := p.client.GetServices(coreDNSPrefix)
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, service := range services { for _, service := range services {
domains := strings.Split(strings.TrimPrefix(service.Key, "/skydns/"), "/") domains := strings.Split(strings.TrimPrefix(service.Key, coreDNSPrefix), "/")
reverse(domains) reverse(domains)
dnsName := strings.Join(domains[service.TargetStrip:], ".") dnsName := strings.Join(domains[service.TargetStrip:], ".")
if !p.domainFilter.Match(dnsName) { if !p.domainFilter.Match(dnsName) {
@ -272,13 +274,14 @@ func (p coreDNSProvider) Records() ([]*endpoint.Endpoint, error) {
} }
prefix := strings.Join(domains[:service.TargetStrip], ".") prefix := strings.Join(domains[:service.TargetStrip], ".")
if service.Host != "" { if service.Host != "" {
ep := endpoint.NewEndpoint( ep := endpoint.NewEndpointWithTTL(
dnsName, dnsName,
guessRecordType(service.Host), guessRecordType(service.Host),
endpoint.TTL(service.TTL),
service.Host, service.Host,
) )
ep.Labels["originalText"] = service.Text ep.Labels["originalText"] = service.Text
ep.Labels["prefix"] = prefix ep.Labels[randomPrefixLabel] = prefix
result = append(result, ep) result = append(result, ep)
} }
if service.Text != "" { if service.Text != "" {
@ -287,20 +290,21 @@ func (p coreDNSProvider) Records() ([]*endpoint.Endpoint, error) {
endpoint.RecordTypeTXT, endpoint.RecordTypeTXT,
service.Text, service.Text,
) )
ep.Labels["prefix"] = prefix ep.Labels[randomPrefixLabel] = prefix
result = append(result, ep) result = append(result, ep)
} }
} }
return result, nil 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 { func (p coreDNSProvider) ApplyChanges(changes *plan.Changes) error {
grouped := map[string][]*endpoint.Endpoint{} grouped := map[string][]*endpoint.Endpoint{}
for _, ep := range changes.Create { for _, ep := range changes.Create {
grouped[ep.DNSName] = append(grouped[ep.DNSName], ep) 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) grouped[ep.DNSName] = append(grouped[ep.DNSName], ep)
} }
for dnsName, group := range grouped { for dnsName, group := range grouped {
@ -313,7 +317,7 @@ func (p coreDNSProvider) ApplyChanges(changes *plan.Changes) error {
if ep.RecordType == endpoint.RecordTypeTXT { if ep.RecordType == endpoint.RecordTypeTXT {
continue continue
} }
prefix := ep.Labels["prefix"] prefix := ep.Labels[randomPrefixLabel]
if prefix == "" { if prefix == "" {
prefix = fmt.Sprintf("%08x", rand.Int31()) prefix = fmt.Sprintf("%08x", rand.Int31())
} }
@ -322,6 +326,7 @@ func (p coreDNSProvider) ApplyChanges(changes *plan.Changes) error {
Text: ep.Labels["originalText"], Text: ep.Labels["originalText"],
Key: etcdKeyFor(prefix + "." + dnsName), Key: etcdKeyFor(prefix + "." + dnsName),
TargetStrip: strings.Count(prefix, ".") + 1, TargetStrip: strings.Count(prefix, ".") + 1,
TTL: uint32(ep.RecordTTL),
} }
services = append(services, service) services = append(services, service)
} }
@ -331,13 +336,14 @@ func (p coreDNSProvider) ApplyChanges(changes *plan.Changes) error {
continue continue
} }
if index >= len(services) { if index >= len(services) {
prefix := ep.Labels["prefix"] prefix := ep.Labels[randomPrefixLabel]
if prefix == "" { if prefix == "" {
prefix = fmt.Sprintf("%08x", rand.Int31()) prefix = fmt.Sprintf("%08x", rand.Int31())
} }
services = append(services, Service{ services = append(services, Service{
Key: etcdKeyFor(prefix + "." + dnsName), Key: etcdKeyFor(prefix + "." + dnsName),
TargetStrip: strings.Count(prefix, ".") + 1, TargetStrip: strings.Count(prefix, ".") + 1,
TTL: uint32(ep.RecordTTL),
}) })
} }
services[index].Text = ep.Targets[0] services[index].Text = ep.Targets[0]
@ -349,7 +355,7 @@ func (p coreDNSProvider) ApplyChanges(changes *plan.Changes) error {
} }
for _, service := range services { 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 { if !p.dryRun {
err := p.client.SaveService(&service) err := p.client.SaveService(&service)
if err != nil { if err != nil {
@ -361,8 +367,8 @@ func (p coreDNSProvider) ApplyChanges(changes *plan.Changes) error {
for _, ep := range changes.Delete { for _, ep := range changes.Delete {
dnsName := ep.DNSName dnsName := ep.DNSName
if ep.Labels["prefix"] != "" { if ep.Labels[randomPrefixLabel] != "" {
dnsName = ep.Labels["prefix"] + "." + dnsName dnsName = ep.Labels[randomPrefixLabel] + "." + dnsName
} }
key := etcdKeyFor(dnsName) key := etcdKeyFor(dnsName)
log.Infof("Delete key %s", key) log.Infof("Delete key %s", key)
@ -387,7 +393,7 @@ func guessRecordType(target string) string {
func etcdKeyFor(dnsName string) string { func etcdKeyFor(dnsName string) string {
domains := strings.Split(dnsName, ".") domains := strings.Split(dnsName, ".")
reverse(domains) reverse(domains)
return "/skydns/" + strings.Join(domains, "/") return coreDNSPrefix + strings.Join(domains, "/")
} }
func reverse(slice []string) { func reverse(slice []string) {

View File

@ -235,8 +235,6 @@ func TestCoreDNSApplyChanges(t *testing.T) {
} }
validateServices(client.services, expectedServices1, t, 1) validateServices(client.services, expectedServices1, t, 1)
updatedEp := endpoint.NewEndpoint("domain1.local", endpoint.RecordTypeA, "6.6.6.6")
updatedEp.Labels["originalText"] = "string1"
changes2 := &plan.Changes{ changes2 := &plan.Changes{
Create: []*endpoint.Endpoint{ Create: []*endpoint.Endpoint{
endpoint.NewEndpoint("domain3.local", endpoint.RecordTypeA, "7.7.7.7"), 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"), 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) applyServiceChanges(coredns, changes2)
expectedServices2 := map[string]*Service{ expectedServices2 := map[string]*Service{

View File

@ -564,7 +564,7 @@ func (d *dynProviderState) commit(client *dynect.Client) error {
err = apiRetryLoop(func() error { err = apiRetryLoop(func() error {
return client.Do("PUT", fmt.Sprintf("Zone/%s/", zone), &zonePublish, &response) 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) { switch len(errs) {
@ -597,7 +597,7 @@ func (d *dynProviderState) Records() ([]*endpoint.Endpoint, error) {
serial, err := d.fetchZoneSerial(client, zone) serial, err := d.fetchZoneSerial(client, zone)
if err != nil { if err != nil {
if strings.Index(err.Error(), "404 Not Found") >= 0 { 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 continue
} }

View File

@ -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 { for zone, ops := range changes {
if len(ops) == 0 { if len(ops) == 0 {
delete(changes, zone) delete(changes, zone)

View File

@ -132,6 +132,7 @@ func stringifyHTTPResponseBody(r *http.Response) (body string) {
// well as mock APIClients used in testing // well as mock APIClients used in testing
type PDNSAPIProvider interface { type PDNSAPIProvider interface {
ListZones() ([]pgo.Zone, *http.Response, error) ListZones() ([]pgo.Zone, *http.Response, error)
PartitionZones(zones []pgo.Zone) ([]pgo.Zone, []pgo.Zone)
ListZone(zoneID string) (pgo.Zone, *http.Response, error) ListZone(zoneID string) (pgo.Zone, *http.Response, error)
PatchZone(zoneID string, zoneStruct pgo.Zone) (*http.Response, error) PatchZone(zoneID string, zoneStruct pgo.Zone) (*http.Response, error)
} }
@ -141,6 +142,7 @@ type PDNSAPIClient struct {
dryRun bool dryRun bool
authCtx context.Context authCtx context.Context
client *pgo.APIClient client *pgo.APIClient
domainFilter DomainFilter
} }
// ListZones : Method returns all enabled zones from PowerDNS // 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) log.Debugf("Retrying ListZones() ... %d", i)
time.Sleep(retryAfterTime * (1 << uint(i))) time.Sleep(retryAfterTime * (1 << uint(i)))
continue continue
} }
return zones, resp, err 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 // 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 // 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) { 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=") 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 // We do not support dry running, exit safely instead of surprising the user
// TODO: Add Dry Run support // TODO: Add Dry Run support
if config.DryRun { if config.DryRun {
@ -241,6 +254,7 @@ func NewPDNSProvider(config PDNSConfig) (*PDNSProvider, error) {
dryRun: config.DryRun, dryRun: config.DryRun,
authCtx: context.WithValue(context.TODO(), pgo.ContextAPIKey, pgo.APIKey{Key: config.APIKey}), authCtx: context.WithValue(context.TODO(), pgo.ContextAPIKey, pgo.APIKey{Key: config.APIKey}),
client: pgo.NewAPIClient(pdnsClientConfig), client: pgo.NewAPIClient(pdnsClientConfig),
domainFilter: config.DomainFilter,
}, },
} }
@ -281,22 +295,23 @@ func (p *PDNSProvider) ConvertEndpointsToZones(eps []*endpoint.Endpoint, changet
if err != nil { if err != nil {
return nil, err return nil, err
} }
filteredZones, residualZones := p.client.PartitionZones(zones)
// Sort the zone by length of the name in descending order, we use this // 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 // 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 // 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 // DNSName in a trie of Zone names, which should be O(Endpoints), but at this point it's not
// necessary. // necessary.
for _, zone := range zones { for _, zone := range filteredZones {
zone.Rrsets = []pgo.RrSet{} zone.Rrsets = []pgo.RrSet{}
for i := 0; i < len(endpoints); { for i := 0; i < len(endpoints); {
ep := endpoints[i] ep := endpoints[i]
dnsname := ensureTrailingDot(ep.DNSName) 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 // The assumption here is that there will only ever be one target
// per (ep.DNSName, ep.RecordType) tuple, which holds true for // per (ep.DNSName, ep.RecordType) tuple, which holds true for
// external-dns v5.0.0-alpha onwards // 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") return nil, errors.New("Value of record TTL overflows, limited to int32")
} }
if ep.RecordTTL == 0 { 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) rrset.Ttl = int32(defaultTTL)
} else { } else {
rrset.Ttl = int32(ep.RecordTTL) 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 // We warn instead of hard fail here because we don't want a misconfig to cause everything to go down
if len(endpoints) > 0 { if len(endpoints) > 0 {
log.Warnf("No matching zones were found for the following endpoints: %+v", endpoints) 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 { if err != nil {
return nil, err return nil, err
} }
filteredZones, _ := p.client.PartitionZones(zones)
for _, zone := range zones { for _, zone := range filteredZones {
z, _, err := p.client.ListZone(zone.Id) z, _, err := p.client.ListZone(zone.Id)
if err != nil { if err != nil {
log.Warnf("Unable to fetch Records") log.Warnf("Unable to fetch Records")

View File

@ -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.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\""), 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{ 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. // 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{}, 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{ ZoneEmptyLong = pgo.Zone{
Id: "long.domainname.example.com.", Id: "long.domainname.example.com.",
Name: "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{ ZoneEmptyToLongPatch = pgo.Zone{
Id: "long.domainname.example.com.", Id: "long.domainname.example.com.",
Name: "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) { func (c *PDNSAPIClientStub) ListZones() ([]pgo.Zone, *http.Response, error) {
return []pgo.Zone{ZoneMixed}, nil, nil 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) { func (c *PDNSAPIClientStub) ListZone(zoneID string) (pgo.Zone, *http.Response, error) {
return ZoneMixed, nil, nil return ZoneMixed, nil, nil
} }
@ -415,6 +505,9 @@ type PDNSAPIClientStubEmptyZones struct {
func (c *PDNSAPIClientStubEmptyZones) ListZones() ([]pgo.Zone, *http.Response, error) { func (c *PDNSAPIClientStubEmptyZones) ListZones() ([]pgo.Zone, *http.Response, error) {
return []pgo.Zone{ZoneEmpty, ZoneEmptyLong, ZoneEmpty2}, nil, nil 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) { func (c *PDNSAPIClientStubEmptyZones) ListZone(zoneID string) (pgo.Zone, *http.Response, error) {
if strings.Contains(zoneID, "example.com") { 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") { } else if strings.Contains(zoneID, "mock.test") {
return ZoneEmpty2, nil, nil return ZoneEmpty2, nil, nil
} else if strings.Contains(zoneID, "long.domainname.example.com") { } else if strings.Contains(zoneID, "long.domainname.example.com") {
return ZoneEmpty2, nil, nil return ZoneEmptyLong, nil, nil
} }
return pgo.Zone{}, 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") 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 { type NewPDNSProviderTestSuite struct {
@ -488,7 +612,7 @@ func (suite *NewPDNSProviderTestSuite) TestPDNSProviderCreate() {
APIKey: "foo", APIKey: "foo",
DomainFilter: NewDomainFilter([]string{"example.com", "example.org"}), 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{ _, err = NewPDNSProvider(PDNSConfig{
Server: "http://localhost:8081", 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() { func (suite *NewPDNSProviderTestSuite) TestPDNSmutateRecords() {
// Function definition: mutateRecords(endpoints []*endpoint.Endpoint, changetype pdnsChangeType) error // Function definition: mutateRecords(endpoints []*endpoint.Endpoint, changetype pdnsChangeType) error
@ -742,6 +911,7 @@ func (suite *NewPDNSProviderTestSuite) TestPDNSmutateRecords() {
assert.NotNil(suite.T(), err) assert.NotNil(suite.T(), err)
} }
func TestNewPDNSProviderTestSuite(t *testing.T) { func TestNewPDNSProviderTestSuite(t *testing.T) {
suite.Run(t, new(NewPDNSProviderTestSuite)) suite.Run(t, new(NewPDNSProviderTestSuite))
} }

View File

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

View File

@ -175,12 +175,14 @@ func (sc *gatewaySource) endpointsFromTemplate(config *istiomodel.Config) ([]*en
} }
} }
providerSpecific := getProviderSpecificAnnotations(config.Annotations)
var endpoints []*endpoint.Endpoint var endpoints []*endpoint.Endpoint
// splits the FQDN template and removes the trailing periods // splits the FQDN template and removes the trailing periods
hostnameList := strings.Split(strings.Replace(hostnames, " ", "", -1), ",") hostnameList := strings.Split(strings.Replace(hostnames, " ", "", -1), ",")
for _, hostname := range hostnameList { for _, hostname := range hostnameList {
hostname = strings.TrimSuffix(hostname, ".") hostname = strings.TrimSuffix(hostname, ".")
endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl)...) endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific)...)
} }
return endpoints, nil return endpoints, nil
} }
@ -259,12 +261,14 @@ func (sc *gatewaySource) endpointsFromGatewayConfig(config istiomodel.Config) ([
gateway := config.Spec.(*istionetworking.Gateway) gateway := config.Spec.(*istionetworking.Gateway)
providerSpecific := getProviderSpecificAnnotations(config.Annotations)
for _, server := range gateway.Servers { for _, server := range gateway.Servers {
for _, host := range server.Hosts { for _, host := range server.Hosts {
if host == "" { if host == "" {
continue 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 { if !sc.ignoreHostnameAnnotation {
hostnameList := getHostnamesFromAnnotations(config.Annotations) hostnameList := getHostnamesFromAnnotations(config.Annotations)
for _, hostname := range hostnameList { for _, hostname := range hostnameList {
endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl)...) endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific)...)
} }
} }

View File

@ -148,12 +148,14 @@ func (sc *ingressSource) endpointsFromTemplate(ing *v1beta1.Ingress) ([]*endpoin
targets = targetsFromIngressStatus(ing.Status) targets = targetsFromIngressStatus(ing.Status)
} }
providerSpecific := getProviderSpecificAnnotations(ing.Annotations)
var endpoints []*endpoint.Endpoint var endpoints []*endpoint.Endpoint
// splits the FQDN template and removes the trailing periods // splits the FQDN template and removes the trailing periods
hostnameList := strings.Split(strings.Replace(hostnames, " ", "", -1), ",") hostnameList := strings.Split(strings.Replace(hostnames, " ", "", -1), ",")
for _, hostname := range hostnameList { for _, hostname := range hostnameList {
hostname = strings.TrimSuffix(hostname, ".") hostname = strings.TrimSuffix(hostname, ".")
endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl)...) endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific)...)
} }
return endpoints, nil return endpoints, nil
} }
@ -210,11 +212,13 @@ func endpointsFromIngress(ing *v1beta1.Ingress, ignoreHostnameAnnotation bool) [
targets = targetsFromIngressStatus(ing.Status) targets = targetsFromIngressStatus(ing.Status)
} }
providerSpecific := getProviderSpecificAnnotations(ing.Annotations)
for _, rule := range ing.Spec.Rules { for _, rule := range ing.Spec.Rules {
if rule.Host == "" { if rule.Host == "" {
continue continue
} }
endpoints = append(endpoints, endpointsForHostname(rule.Host, targets, ttl)...) endpoints = append(endpoints, endpointsForHostname(rule.Host, targets, ttl, providerSpecific)...)
} }
for _, tls := range ing.Spec.TLS { for _, tls := range ing.Spec.TLS {
@ -222,7 +226,7 @@ func endpointsFromIngress(ing *v1beta1.Ingress, ignoreHostnameAnnotation bool) [
if host == "" { if host == "" {
continue 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 { if !ignoreHostnameAnnotation {
hostnameList := getHostnamesFromAnnotations(ing.Annotations) hostnameList := getHostnamesFromAnnotations(ing.Annotations)
for _, hostname := range hostnameList { for _, hostname := range hostnameList {
endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl)...) endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific)...)
} }
} }
return endpoints return endpoints

View File

@ -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", title: "template for ingress with annotation",
targetNamespace: "", targetNamespace: "",

View File

@ -35,6 +35,8 @@ const (
targetAnnotationKey = "external-dns.alpha.kubernetes.io/target" targetAnnotationKey = "external-dns.alpha.kubernetes.io/target"
// The annotation used for defining the desired DNS record TTL // The annotation used for defining the desired DNS record TTL
ttlAnnotationKey = "external-dns.alpha.kubernetes.io/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 // The value of the controller annotation so that we feel responsible
controllerAnnotationValue = "dns-controller" controllerAnnotationValue = "dns-controller"
) )
@ -74,6 +76,18 @@ func getHostnamesFromAnnotations(annotations map[string]string) []string {
return strings.Split(strings.Replace(hostnameAnnotation, " ", "", -1), ",") 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. // getTargetsFromTargetAnnotation gets endpoints from optional "target" annotation.
// Returns empty endpoints array if none are found. // Returns empty endpoints array if none are found.
func getTargetsFromTargetAnnotation(annotations map[string]string) endpoint.Targets { 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. // 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 endpoints []*endpoint.Endpoint
var aTargets endpoint.Targets var aTargets endpoint.Targets
@ -124,6 +138,7 @@ func endpointsForHostname(hostname string, targets endpoint.Targets, ttl endpoin
RecordTTL: ttl, RecordTTL: ttl,
RecordType: endpoint.RecordTypeA, RecordType: endpoint.RecordTypeA,
Labels: endpoint.NewLabels(), Labels: endpoint.NewLabels(),
ProviderSpecific: providerSpecific,
} }
endpoints = append(endpoints, epA) endpoints = append(endpoints, epA)
} }
@ -135,6 +150,7 @@ func endpointsForHostname(hostname string, targets endpoint.Targets, ttl endpoin
RecordTTL: ttl, RecordTTL: ttl,
RecordType: endpoint.RecordTypeCNAME, RecordType: endpoint.RecordTypeCNAME,
Labels: endpoint.NewLabels(), Labels: endpoint.NewLabels(),
ProviderSpecific: providerSpecific,
} }
endpoints = append(endpoints, epCNAME) endpoints = append(endpoints, epCNAME)
} }