From 770223bf6d289956cd28a529f239d77571879751 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 6 Sep 2023 06:58:55 +0000 Subject: [PATCH] Deployed 0483ffde to v0.13.6 with MkDocs 1.3.0 and mike 1.1.2 --- .../index.html | 6 +- latest/404.html | 6 +- latest/CONTRIBUTING/index.html | 6 +- latest/LICENSE/index.html | 6 +- latest/annotations/annotations/index.html | 16 + latest/code-of-conduct/index.html | 6 +- latest/contributing/chart/index.html | 6 +- latest/contributing/crd-source/index.html | 6 +- .../contributing/getting-started/index.html | 6 +- .../sources-and-providers/index.html | 6 +- latest/faq/index.html | 6 +- latest/index.html | 6 +- latest/initial-design/index.html | 6 +- latest/proposal/multi-target/index.html | 6 +- latest/proposal/registry/index.html | 16 - latest/registry/dynamodb/index.html | 16 + latest/registry/index.html | 16 - latest/registry/registry/index.html | 16 + latest/registry/txt/index.html | 16 + latest/release/index.html | 6 +- latest/sources/ingress/index.html | 16 + latest/sources/sources/index.html | 16 + latest/ttl/index.html | 6 +- latest/tutorials/ANS_Group_SafeDNS/index.html | 6 +- latest/tutorials/akamai-edgedns/index.html | 6 +- latest/tutorials/alibabacloud/index.html | 6 +- .../aws-load-balancer-controller/index.html | 6 +- latest/tutorials/aws-sd/index.html | 6 +- latest/tutorials/aws/index.html | 6 +- latest/tutorials/azure-private-dns/index.html | 6 +- latest/tutorials/azure/index.html | 6 +- latest/tutorials/bluecat/index.html | 6 +- latest/tutorials/civo/index.html | 6 +- latest/tutorials/cloudflare/index.html | 6 +- latest/tutorials/contour/index.html | 6 +- latest/tutorials/coredns/index.html | 6 +- latest/tutorials/designate/index.html | 6 +- latest/tutorials/digitalocean/index.html | 6 +- latest/tutorials/dnsimple/index.html | 6 +- latest/tutorials/dyn/index.html | 6 +- latest/tutorials/exoscale/index.html | 6 +- latest/tutorials/externalname/index.html | 6 +- latest/tutorials/f5-virtualserver/index.html | 6 +- latest/tutorials/gandi/index.html | 6 +- latest/tutorials/gateway-api/index.html | 6 +- latest/tutorials/gke/index.html | 6 +- latest/tutorials/gloo-proxy/index.html | 6 +- latest/tutorials/godaddy/index.html | 6 +- latest/tutorials/hostport/index.html | 6 +- latest/tutorials/ibmcloud/index.html | 6 +- latest/tutorials/infoblox/index.html | 6 +- latest/tutorials/istio/index.html | 6 +- latest/tutorials/kong/index.html | 6 +- .../tutorials/kops-dns-controller/index.html | 6 +- latest/tutorials/kube-ingress-aws/index.html | 6 +- latest/tutorials/linode/index.html | 6 +- latest/tutorials/mx-record/index.html | 6 +- latest/tutorials/nginx-ingress/index.html | 6 +- latest/tutorials/nodes/index.html | 6 +- latest/tutorials/ns-record/index.html | 6 +- latest/tutorials/ns1/index.html | 6 +- latest/tutorials/openshift/index.html | 6 +- latest/tutorials/oracle/index.html | 6 +- latest/tutorials/ovh/index.html | 6 +- latest/tutorials/pdns/index.html | 6 +- latest/tutorials/pihole/index.html | 6 +- latest/tutorials/plural/index.html | 6 +- .../public-private-route53/index.html | 6 +- latest/tutorials/rcodezero/index.html | 6 +- latest/tutorials/rdns/index.html | 6 +- latest/tutorials/rfc2136/index.html | 6 +- latest/tutorials/scaleway/index.html | 6 +- latest/tutorials/security-context/index.html | 6 +- latest/tutorials/tencentcloud/index.html | 6 +- latest/tutorials/traefik-proxy/index.html | 16 + latest/tutorials/transip/index.html | 6 +- latest/tutorials/ultradns/index.html | 6 +- latest/tutorials/vinyldns/index.html | 6 +- latest/tutorials/vultr/index.html | 6 +- .../index.html | 2296 ++++++ v0.13.6/404.html | 1841 +++++ v0.13.6/CONTRIBUTING/index.html | 2024 +++++ v0.13.6/LICENSE/index.html | 2100 ++++++ v0.13.6/annotations/annotations/index.html | 2386 ++++++ v0.13.6/assets/images/favicon.png | Bin 0 -> 1870 bytes .../assets/javascripts/bundle.c44cc438.min.js | 29 + .../javascripts/bundle.c44cc438.min.js.map | 8 + .../javascripts/lunr/min/lunr.ar.min.js | 1 + .../javascripts/lunr/min/lunr.da.min.js | 18 + .../javascripts/lunr/min/lunr.de.min.js | 18 + .../javascripts/lunr/min/lunr.du.min.js | 18 + .../javascripts/lunr/min/lunr.es.min.js | 18 + .../javascripts/lunr/min/lunr.fi.min.js | 18 + .../javascripts/lunr/min/lunr.fr.min.js | 18 + .../javascripts/lunr/min/lunr.hi.min.js | 1 + .../javascripts/lunr/min/lunr.hu.min.js | 18 + .../javascripts/lunr/min/lunr.it.min.js | 18 + .../javascripts/lunr/min/lunr.ja.min.js | 1 + .../javascripts/lunr/min/lunr.jp.min.js | 1 + .../javascripts/lunr/min/lunr.multi.min.js | 1 + .../javascripts/lunr/min/lunr.nl.min.js | 18 + .../javascripts/lunr/min/lunr.no.min.js | 18 + .../javascripts/lunr/min/lunr.pt.min.js | 18 + .../javascripts/lunr/min/lunr.ro.min.js | 18 + .../javascripts/lunr/min/lunr.ru.min.js | 18 + .../lunr/min/lunr.stemmer.support.min.js | 1 + .../javascripts/lunr/min/lunr.sv.min.js | 18 + .../javascripts/lunr/min/lunr.th.min.js | 1 + .../javascripts/lunr/min/lunr.tr.min.js | 18 + .../javascripts/lunr/min/lunr.vi.min.js | 1 + .../javascripts/lunr/min/lunr.zh.min.js | 1 + v0.13.6/assets/javascripts/lunr/tinyseg.js | 206 + v0.13.6/assets/javascripts/lunr/wordcut.js | 6708 +++++++++++++++++ .../workers/search.5e67fbfe.min.js | 48 + .../workers/search.5e67fbfe.min.js.map | 8 + .../assets/stylesheets/main.644de097.min.css | 1 + .../stylesheets/main.644de097.min.css.map | 1 + .../stylesheets/palette.e6a45f82.min.css | 1 + .../stylesheets/palette.e6a45f82.min.css.map | 1 + v0.13.6/code-of-conduct/index.html | 1933 +++++ v0.13.6/contributing/chart/index.html | 1981 +++++ .../contributing/crd-source/crd-manifest.yaml | 94 + .../crd-source/dnsendpoint-aws-example.yaml | 18 + .../crd-source/dnsendpoint-example.yaml | 15 + v0.13.6/contributing/crd-source/index.html | 2139 ++++++ .../contributing/getting-started/index.html | 1971 +++++ .../sources-and-providers/index.html | 2034 +++++ v0.13.6/faq/index.html | 2637 +++++++ v0.13.6/img/external-dns.png | Bin 0 -> 256840 bytes v0.13.6/index.html | 2501 ++++++ v0.13.6/initial-design/index.html | 2254 ++++++ v0.13.6/proposal/multi-target/index.html | 2076 +++++ v0.13.6/registry/dynamodb/index.html | 2055 +++++ v0.13.6/registry/registry/index.html | 1991 +++++ v0.13.6/registry/txt/index.html | 2127 ++++++ v0.13.6/release/index.html | 2125 ++++++ v0.13.6/scripts/copy_docs.sh | 9 + v0.13.6/scripts/docs.go | 48 + v0.13.6/scripts/index.html.gotmpl | 3 + v0.13.6/scripts/requirements.txt | 5 + v0.13.6/search/search_index.json | 1 + v0.13.6/sitemap.xml | 383 + v0.13.6/sitemap.xml.gz | Bin 0 -> 244 bytes v0.13.6/sources/ingress/index.html | 2050 +++++ v0.13.6/sources/sources/index.html | 2076 +++++ v0.13.6/ttl/index.html | 1999 +++++ .../tutorials/ANS_Group_SafeDNS/index.html | 2261 ++++++ v0.13.6/tutorials/akamai-edgedns/index.html | 2386 ++++++ v0.13.6/tutorials/alibabacloud/index.html | 2476 ++++++ .../aws-load-balancer-controller/index.html | 2162 ++++++ v0.13.6/tutorials/aws-sd/index.html | 2318 ++++++ v0.13.6/tutorials/aws/index.html | 3394 +++++++++ .../tutorials/azure-private-dns/index.html | 2517 +++++++ v0.13.6/tutorials/azure/index.html | 3183 ++++++++ v0.13.6/tutorials/bluecat/index.html | 2255 ++++++ v0.13.6/tutorials/civo/index.html | 2239 ++++++ v0.13.6/tutorials/cloudflare/index.html | 2297 ++++++ v0.13.6/tutorials/contour/index.html | 2243 ++++++ v0.13.6/tutorials/coredns/index.html | 2355 ++++++ v0.13.6/tutorials/designate/index.html | 2322 ++++++ v0.13.6/tutorials/digitalocean/index.html | 2290 ++++++ v0.13.6/tutorials/dnsimple/index.html | 2332 ++++++ v0.13.6/tutorials/dyn/index.html | 2164 ++++++ v0.13.6/tutorials/exoscale/index.html | 2167 ++++++ v0.13.6/tutorials/externalname/index.html | 2110 ++++++ v0.13.6/tutorials/f5-virtualserver/index.html | 2008 +++++ v0.13.6/tutorials/gandi/index.html | 2243 ++++++ v0.13.6/tutorials/gateway-api/index.html | 2094 +++++ v0.13.6/tutorials/gke/index.html | 2737 +++++++ v0.13.6/tutorials/gloo-proxy/index.html | 2086 +++++ v0.13.6/tutorials/godaddy/index.html | 2275 ++++++ v0.13.6/tutorials/hostport/index.html | 2320 ++++++ v0.13.6/tutorials/ibmcloud/index.html | 2409 ++++++ v0.13.6/tutorials/infoblox/index.html | 2362 ++++++ v0.13.6/tutorials/istio/index.html | 2487 ++++++ v0.13.6/tutorials/kong/index.html | 2081 +++++ .../tutorials/kops-dns-controller/index.html | 2063 +++++ v0.13.6/tutorials/kube-ingress-aws/index.html | 2335 ++++++ v0.13.6/tutorials/linode/index.html | 2239 ++++++ v0.13.6/tutorials/mx-record/index.html | 1952 +++++ v0.13.6/tutorials/nginx-ingress/index.html | 2782 +++++++ v0.13.6/tutorials/nodes/index.html | 2098 ++++++ v0.13.6/tutorials/ns-record/index.html | 1949 +++++ v0.13.6/tutorials/ns1/index.html | 2314 ++++++ v0.13.6/tutorials/openshift/index.html | 2284 ++++++ v0.13.6/tutorials/oracle/index.html | 2218 ++++++ v0.13.6/tutorials/ovh/index.html | 2324 ++++++ v0.13.6/tutorials/pdns/index.html | 2217 ++++++ v0.13.6/tutorials/pihole/index.html | 2234 ++++++ v0.13.6/tutorials/plural/index.html | 2239 ++++++ .../public-private-route53/index.html | 2387 ++++++ v0.13.6/tutorials/rcodezero/index.html | 2266 ++++++ v0.13.6/tutorials/rdns/index.html | 2280 ++++++ v0.13.6/tutorials/rfc2136/index.html | 2586 +++++++ v0.13.6/tutorials/scaleway/index.html | 2252 ++++++ v0.13.6/tutorials/security-context/index.html | 1960 +++++ v0.13.6/tutorials/tencentcloud/index.html | 2207 ++++++ v0.13.6/tutorials/traefik-proxy/index.html | 2140 ++++++ v0.13.6/tutorials/transip/index.html | 2217 ++++++ v0.13.6/tutorials/ultradns/index.html | 2770 +++++++ v0.13.6/tutorials/vinyldns/index.html | 2205 ++++++ v0.13.6/tutorials/vultr/index.html | 2240 ++++++ versions.json | 2 +- 203 files changed, 182116 insertions(+), 243 deletions(-) create mode 100644 latest/annotations/annotations/index.html delete mode 100644 latest/proposal/registry/index.html create mode 100644 latest/registry/dynamodb/index.html delete mode 100644 latest/registry/index.html create mode 100644 latest/registry/registry/index.html create mode 100644 latest/registry/txt/index.html create mode 100644 latest/sources/ingress/index.html create mode 100644 latest/sources/sources/index.html create mode 100644 latest/tutorials/traefik-proxy/index.html create mode 100644 v0.13.6/20190708-external-dns-incubator/index.html create mode 100644 v0.13.6/404.html create mode 100644 v0.13.6/CONTRIBUTING/index.html create mode 100644 v0.13.6/LICENSE/index.html create mode 100644 v0.13.6/annotations/annotations/index.html create mode 100644 v0.13.6/assets/images/favicon.png create mode 100644 v0.13.6/assets/javascripts/bundle.c44cc438.min.js create mode 100644 v0.13.6/assets/javascripts/bundle.c44cc438.min.js.map create mode 100644 v0.13.6/assets/javascripts/lunr/min/lunr.ar.min.js create mode 100644 v0.13.6/assets/javascripts/lunr/min/lunr.da.min.js create mode 100644 v0.13.6/assets/javascripts/lunr/min/lunr.de.min.js create mode 100644 v0.13.6/assets/javascripts/lunr/min/lunr.du.min.js create mode 100644 v0.13.6/assets/javascripts/lunr/min/lunr.es.min.js create mode 100644 v0.13.6/assets/javascripts/lunr/min/lunr.fi.min.js create mode 100644 v0.13.6/assets/javascripts/lunr/min/lunr.fr.min.js create mode 100644 v0.13.6/assets/javascripts/lunr/min/lunr.hi.min.js create mode 100644 v0.13.6/assets/javascripts/lunr/min/lunr.hu.min.js create mode 100644 v0.13.6/assets/javascripts/lunr/min/lunr.it.min.js create mode 100644 v0.13.6/assets/javascripts/lunr/min/lunr.ja.min.js create mode 100644 v0.13.6/assets/javascripts/lunr/min/lunr.jp.min.js create mode 100644 v0.13.6/assets/javascripts/lunr/min/lunr.multi.min.js create mode 100644 v0.13.6/assets/javascripts/lunr/min/lunr.nl.min.js create mode 100644 v0.13.6/assets/javascripts/lunr/min/lunr.no.min.js create mode 100644 v0.13.6/assets/javascripts/lunr/min/lunr.pt.min.js create mode 100644 v0.13.6/assets/javascripts/lunr/min/lunr.ro.min.js create mode 100644 v0.13.6/assets/javascripts/lunr/min/lunr.ru.min.js create mode 100644 v0.13.6/assets/javascripts/lunr/min/lunr.stemmer.support.min.js create mode 100644 v0.13.6/assets/javascripts/lunr/min/lunr.sv.min.js create mode 100644 v0.13.6/assets/javascripts/lunr/min/lunr.th.min.js create mode 100644 v0.13.6/assets/javascripts/lunr/min/lunr.tr.min.js create mode 100644 v0.13.6/assets/javascripts/lunr/min/lunr.vi.min.js create mode 100644 v0.13.6/assets/javascripts/lunr/min/lunr.zh.min.js create mode 100644 v0.13.6/assets/javascripts/lunr/tinyseg.js create mode 100644 v0.13.6/assets/javascripts/lunr/wordcut.js create mode 100644 v0.13.6/assets/javascripts/workers/search.5e67fbfe.min.js create mode 100644 v0.13.6/assets/javascripts/workers/search.5e67fbfe.min.js.map create mode 100644 v0.13.6/assets/stylesheets/main.644de097.min.css create mode 100644 v0.13.6/assets/stylesheets/main.644de097.min.css.map create mode 100644 v0.13.6/assets/stylesheets/palette.e6a45f82.min.css create mode 100644 v0.13.6/assets/stylesheets/palette.e6a45f82.min.css.map create mode 100644 v0.13.6/code-of-conduct/index.html create mode 100644 v0.13.6/contributing/chart/index.html create mode 100644 v0.13.6/contributing/crd-source/crd-manifest.yaml create mode 100644 v0.13.6/contributing/crd-source/dnsendpoint-aws-example.yaml create mode 100644 v0.13.6/contributing/crd-source/dnsendpoint-example.yaml create mode 100644 v0.13.6/contributing/crd-source/index.html create mode 100644 v0.13.6/contributing/getting-started/index.html create mode 100644 v0.13.6/contributing/sources-and-providers/index.html create mode 100644 v0.13.6/faq/index.html create mode 100644 v0.13.6/img/external-dns.png create mode 100644 v0.13.6/index.html create mode 100644 v0.13.6/initial-design/index.html create mode 100644 v0.13.6/proposal/multi-target/index.html create mode 100644 v0.13.6/registry/dynamodb/index.html create mode 100644 v0.13.6/registry/registry/index.html create mode 100644 v0.13.6/registry/txt/index.html create mode 100644 v0.13.6/release/index.html create mode 100644 v0.13.6/scripts/copy_docs.sh create mode 100644 v0.13.6/scripts/docs.go create mode 100644 v0.13.6/scripts/index.html.gotmpl create mode 100644 v0.13.6/scripts/requirements.txt create mode 100644 v0.13.6/search/search_index.json create mode 100644 v0.13.6/sitemap.xml create mode 100644 v0.13.6/sitemap.xml.gz create mode 100644 v0.13.6/sources/ingress/index.html create mode 100644 v0.13.6/sources/sources/index.html create mode 100644 v0.13.6/ttl/index.html create mode 100644 v0.13.6/tutorials/ANS_Group_SafeDNS/index.html create mode 100644 v0.13.6/tutorials/akamai-edgedns/index.html create mode 100644 v0.13.6/tutorials/alibabacloud/index.html create mode 100644 v0.13.6/tutorials/aws-load-balancer-controller/index.html create mode 100644 v0.13.6/tutorials/aws-sd/index.html create mode 100644 v0.13.6/tutorials/aws/index.html create mode 100644 v0.13.6/tutorials/azure-private-dns/index.html create mode 100644 v0.13.6/tutorials/azure/index.html create mode 100644 v0.13.6/tutorials/bluecat/index.html create mode 100644 v0.13.6/tutorials/civo/index.html create mode 100644 v0.13.6/tutorials/cloudflare/index.html create mode 100644 v0.13.6/tutorials/contour/index.html create mode 100644 v0.13.6/tutorials/coredns/index.html create mode 100644 v0.13.6/tutorials/designate/index.html create mode 100644 v0.13.6/tutorials/digitalocean/index.html create mode 100644 v0.13.6/tutorials/dnsimple/index.html create mode 100644 v0.13.6/tutorials/dyn/index.html create mode 100644 v0.13.6/tutorials/exoscale/index.html create mode 100644 v0.13.6/tutorials/externalname/index.html create mode 100644 v0.13.6/tutorials/f5-virtualserver/index.html create mode 100644 v0.13.6/tutorials/gandi/index.html create mode 100644 v0.13.6/tutorials/gateway-api/index.html create mode 100644 v0.13.6/tutorials/gke/index.html create mode 100644 v0.13.6/tutorials/gloo-proxy/index.html create mode 100644 v0.13.6/tutorials/godaddy/index.html create mode 100644 v0.13.6/tutorials/hostport/index.html create mode 100644 v0.13.6/tutorials/ibmcloud/index.html create mode 100644 v0.13.6/tutorials/infoblox/index.html create mode 100644 v0.13.6/tutorials/istio/index.html create mode 100644 v0.13.6/tutorials/kong/index.html create mode 100644 v0.13.6/tutorials/kops-dns-controller/index.html create mode 100644 v0.13.6/tutorials/kube-ingress-aws/index.html create mode 100644 v0.13.6/tutorials/linode/index.html create mode 100644 v0.13.6/tutorials/mx-record/index.html create mode 100644 v0.13.6/tutorials/nginx-ingress/index.html create mode 100644 v0.13.6/tutorials/nodes/index.html create mode 100644 v0.13.6/tutorials/ns-record/index.html create mode 100644 v0.13.6/tutorials/ns1/index.html create mode 100644 v0.13.6/tutorials/openshift/index.html create mode 100644 v0.13.6/tutorials/oracle/index.html create mode 100644 v0.13.6/tutorials/ovh/index.html create mode 100644 v0.13.6/tutorials/pdns/index.html create mode 100644 v0.13.6/tutorials/pihole/index.html create mode 100644 v0.13.6/tutorials/plural/index.html create mode 100644 v0.13.6/tutorials/public-private-route53/index.html create mode 100644 v0.13.6/tutorials/rcodezero/index.html create mode 100644 v0.13.6/tutorials/rdns/index.html create mode 100644 v0.13.6/tutorials/rfc2136/index.html create mode 100644 v0.13.6/tutorials/scaleway/index.html create mode 100644 v0.13.6/tutorials/security-context/index.html create mode 100644 v0.13.6/tutorials/tencentcloud/index.html create mode 100644 v0.13.6/tutorials/traefik-proxy/index.html create mode 100644 v0.13.6/tutorials/transip/index.html create mode 100644 v0.13.6/tutorials/ultradns/index.html create mode 100644 v0.13.6/tutorials/vinyldns/index.html create mode 100644 v0.13.6/tutorials/vultr/index.html diff --git a/latest/20190708-external-dns-incubator/index.html b/latest/20190708-external-dns-incubator/index.html index 5078636aa3..de8762cd2b 100644 --- a/latest/20190708-external-dns-incubator/index.html +++ b/latest/20190708-external-dns-incubator/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../v0.13.5/20190708-external-dns-incubator/... + Redirecting to ../../v0.13.6/20190708-external-dns-incubator/... \ No newline at end of file diff --git a/latest/404.html b/latest/404.html index d82afd4c6d..09532fa5e6 100644 --- a/latest/404.html +++ b/latest/404.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../v0.13.5/404.html... + Redirecting to ../v0.13.6/404.html... \ No newline at end of file diff --git a/latest/CONTRIBUTING/index.html b/latest/CONTRIBUTING/index.html index 786c0dbdde..d4ce1ccc53 100644 --- a/latest/CONTRIBUTING/index.html +++ b/latest/CONTRIBUTING/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../v0.13.5/CONTRIBUTING/... + Redirecting to ../../v0.13.6/CONTRIBUTING/... \ No newline at end of file diff --git a/latest/LICENSE/index.html b/latest/LICENSE/index.html index 866b4adb98..72d6b74b9c 100644 --- a/latest/LICENSE/index.html +++ b/latest/LICENSE/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../v0.13.5/LICENSE/... + Redirecting to ../../v0.13.6/LICENSE/... \ No newline at end of file diff --git a/latest/annotations/annotations/index.html b/latest/annotations/annotations/index.html new file mode 100644 index 0000000000..6b7a0ca5c5 --- /dev/null +++ b/latest/annotations/annotations/index.html @@ -0,0 +1,16 @@ + + + + + Redirecting + + + + + Redirecting to ../../../v0.13.6/annotations/annotations/... + + \ No newline at end of file diff --git a/latest/code-of-conduct/index.html b/latest/code-of-conduct/index.html index 716b086a7e..267cc89178 100644 --- a/latest/code-of-conduct/index.html +++ b/latest/code-of-conduct/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../v0.13.5/code-of-conduct/... + Redirecting to ../../v0.13.6/code-of-conduct/... \ No newline at end of file diff --git a/latest/contributing/chart/index.html b/latest/contributing/chart/index.html index 9214b4463d..226565e16b 100644 --- a/latest/contributing/chart/index.html +++ b/latest/contributing/chart/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/contributing/chart/... + Redirecting to ../../../v0.13.6/contributing/chart/... \ No newline at end of file diff --git a/latest/contributing/crd-source/index.html b/latest/contributing/crd-source/index.html index 4fc5c1a423..a9360e1ac6 100644 --- a/latest/contributing/crd-source/index.html +++ b/latest/contributing/crd-source/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/contributing/crd-source/... + Redirecting to ../../../v0.13.6/contributing/crd-source/... \ No newline at end of file diff --git a/latest/contributing/getting-started/index.html b/latest/contributing/getting-started/index.html index 861e83b76f..bfccb71686 100644 --- a/latest/contributing/getting-started/index.html +++ b/latest/contributing/getting-started/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/contributing/getting-started/... + Redirecting to ../../../v0.13.6/contributing/getting-started/... \ No newline at end of file diff --git a/latest/contributing/sources-and-providers/index.html b/latest/contributing/sources-and-providers/index.html index 106247aa58..d6e70d93a7 100644 --- a/latest/contributing/sources-and-providers/index.html +++ b/latest/contributing/sources-and-providers/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/contributing/sources-and-providers/... + Redirecting to ../../../v0.13.6/contributing/sources-and-providers/... \ No newline at end of file diff --git a/latest/faq/index.html b/latest/faq/index.html index bd8628eb91..d6600442d6 100644 --- a/latest/faq/index.html +++ b/latest/faq/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../v0.13.5/faq/... + Redirecting to ../../v0.13.6/faq/... \ No newline at end of file diff --git a/latest/index.html b/latest/index.html index b50a2ee797..ff207c5eeb 100644 --- a/latest/index.html +++ b/latest/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../v0.13.5/... + Redirecting to ../v0.13.6/... \ No newline at end of file diff --git a/latest/initial-design/index.html b/latest/initial-design/index.html index d5638c3433..ae10dc92c1 100644 --- a/latest/initial-design/index.html +++ b/latest/initial-design/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../v0.13.5/initial-design/... + Redirecting to ../../v0.13.6/initial-design/... \ No newline at end of file diff --git a/latest/proposal/multi-target/index.html b/latest/proposal/multi-target/index.html index 65a5fe36be..f6d9f824f3 100644 --- a/latest/proposal/multi-target/index.html +++ b/latest/proposal/multi-target/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/proposal/multi-target/... + Redirecting to ../../../v0.13.6/proposal/multi-target/... \ No newline at end of file diff --git a/latest/proposal/registry/index.html b/latest/proposal/registry/index.html deleted file mode 100644 index af24925df1..0000000000 --- a/latest/proposal/registry/index.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - - Redirecting - - - - - Redirecting to ../../../v0.13.5/proposal/registry/... - - \ No newline at end of file diff --git a/latest/registry/dynamodb/index.html b/latest/registry/dynamodb/index.html new file mode 100644 index 0000000000..4969cbcd20 --- /dev/null +++ b/latest/registry/dynamodb/index.html @@ -0,0 +1,16 @@ + + + + + Redirecting + + + + + Redirecting to ../../../v0.13.6/registry/dynamodb/... + + \ No newline at end of file diff --git a/latest/registry/index.html b/latest/registry/index.html deleted file mode 100644 index e623824c77..0000000000 --- a/latest/registry/index.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - - Redirecting - - - - - Redirecting to ../../v0.13.5/registry/... - - \ No newline at end of file diff --git a/latest/registry/registry/index.html b/latest/registry/registry/index.html new file mode 100644 index 0000000000..45a5e4f7ba --- /dev/null +++ b/latest/registry/registry/index.html @@ -0,0 +1,16 @@ + + + + + Redirecting + + + + + Redirecting to ../../../v0.13.6/registry/registry/... + + \ No newline at end of file diff --git a/latest/registry/txt/index.html b/latest/registry/txt/index.html new file mode 100644 index 0000000000..8088694f16 --- /dev/null +++ b/latest/registry/txt/index.html @@ -0,0 +1,16 @@ + + + + + Redirecting + + + + + Redirecting to ../../../v0.13.6/registry/txt/... + + \ No newline at end of file diff --git a/latest/release/index.html b/latest/release/index.html index 45296d8453..397434eed4 100644 --- a/latest/release/index.html +++ b/latest/release/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../v0.13.5/release/... + Redirecting to ../../v0.13.6/release/... \ No newline at end of file diff --git a/latest/sources/ingress/index.html b/latest/sources/ingress/index.html new file mode 100644 index 0000000000..5113d8cdc4 --- /dev/null +++ b/latest/sources/ingress/index.html @@ -0,0 +1,16 @@ + + + + + Redirecting + + + + + Redirecting to ../../../v0.13.6/sources/ingress/... + + \ No newline at end of file diff --git a/latest/sources/sources/index.html b/latest/sources/sources/index.html new file mode 100644 index 0000000000..deaa83a17a --- /dev/null +++ b/latest/sources/sources/index.html @@ -0,0 +1,16 @@ + + + + + Redirecting + + + + + Redirecting to ../../../v0.13.6/sources/sources/... + + \ No newline at end of file diff --git a/latest/ttl/index.html b/latest/ttl/index.html index 209b4b042d..66a1c38902 100644 --- a/latest/ttl/index.html +++ b/latest/ttl/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../v0.13.5/ttl/... + Redirecting to ../../v0.13.6/ttl/... \ No newline at end of file diff --git a/latest/tutorials/ANS_Group_SafeDNS/index.html b/latest/tutorials/ANS_Group_SafeDNS/index.html index 261114c08c..47972dfa98 100644 --- a/latest/tutorials/ANS_Group_SafeDNS/index.html +++ b/latest/tutorials/ANS_Group_SafeDNS/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/ANS_Group_SafeDNS/... + Redirecting to ../../../v0.13.6/tutorials/ANS_Group_SafeDNS/... \ No newline at end of file diff --git a/latest/tutorials/akamai-edgedns/index.html b/latest/tutorials/akamai-edgedns/index.html index d0632b3365..5ad7063404 100644 --- a/latest/tutorials/akamai-edgedns/index.html +++ b/latest/tutorials/akamai-edgedns/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/akamai-edgedns/... + Redirecting to ../../../v0.13.6/tutorials/akamai-edgedns/... \ No newline at end of file diff --git a/latest/tutorials/alibabacloud/index.html b/latest/tutorials/alibabacloud/index.html index 6c052a76be..a5cdf91182 100644 --- a/latest/tutorials/alibabacloud/index.html +++ b/latest/tutorials/alibabacloud/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/alibabacloud/... + Redirecting to ../../../v0.13.6/tutorials/alibabacloud/... \ No newline at end of file diff --git a/latest/tutorials/aws-load-balancer-controller/index.html b/latest/tutorials/aws-load-balancer-controller/index.html index 61115323c4..3864afa30d 100644 --- a/latest/tutorials/aws-load-balancer-controller/index.html +++ b/latest/tutorials/aws-load-balancer-controller/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/aws-load-balancer-controller/... + Redirecting to ../../../v0.13.6/tutorials/aws-load-balancer-controller/... \ No newline at end of file diff --git a/latest/tutorials/aws-sd/index.html b/latest/tutorials/aws-sd/index.html index cdd9fe1b34..aec6cbb5a7 100644 --- a/latest/tutorials/aws-sd/index.html +++ b/latest/tutorials/aws-sd/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/aws-sd/... + Redirecting to ../../../v0.13.6/tutorials/aws-sd/... \ No newline at end of file diff --git a/latest/tutorials/aws/index.html b/latest/tutorials/aws/index.html index fc84c25caa..767d4b61a7 100644 --- a/latest/tutorials/aws/index.html +++ b/latest/tutorials/aws/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/aws/... + Redirecting to ../../../v0.13.6/tutorials/aws/... \ No newline at end of file diff --git a/latest/tutorials/azure-private-dns/index.html b/latest/tutorials/azure-private-dns/index.html index ea56bb9412..8980986b2e 100644 --- a/latest/tutorials/azure-private-dns/index.html +++ b/latest/tutorials/azure-private-dns/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/azure-private-dns/... + Redirecting to ../../../v0.13.6/tutorials/azure-private-dns/... \ No newline at end of file diff --git a/latest/tutorials/azure/index.html b/latest/tutorials/azure/index.html index 83ce0a013d..bcd30b0e98 100644 --- a/latest/tutorials/azure/index.html +++ b/latest/tutorials/azure/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/azure/... + Redirecting to ../../../v0.13.6/tutorials/azure/... \ No newline at end of file diff --git a/latest/tutorials/bluecat/index.html b/latest/tutorials/bluecat/index.html index e0206baac1..5bb3ebd1d7 100644 --- a/latest/tutorials/bluecat/index.html +++ b/latest/tutorials/bluecat/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/bluecat/... + Redirecting to ../../../v0.13.6/tutorials/bluecat/... \ No newline at end of file diff --git a/latest/tutorials/civo/index.html b/latest/tutorials/civo/index.html index 0de35ea63e..8b0ac0190d 100644 --- a/latest/tutorials/civo/index.html +++ b/latest/tutorials/civo/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/civo/... + Redirecting to ../../../v0.13.6/tutorials/civo/... \ No newline at end of file diff --git a/latest/tutorials/cloudflare/index.html b/latest/tutorials/cloudflare/index.html index e0e7f0e63b..0931cc9d0c 100644 --- a/latest/tutorials/cloudflare/index.html +++ b/latest/tutorials/cloudflare/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/cloudflare/... + Redirecting to ../../../v0.13.6/tutorials/cloudflare/... \ No newline at end of file diff --git a/latest/tutorials/contour/index.html b/latest/tutorials/contour/index.html index c086efa5fc..32afc671be 100644 --- a/latest/tutorials/contour/index.html +++ b/latest/tutorials/contour/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/contour/... + Redirecting to ../../../v0.13.6/tutorials/contour/... \ No newline at end of file diff --git a/latest/tutorials/coredns/index.html b/latest/tutorials/coredns/index.html index 419c512b7f..69469b7b68 100644 --- a/latest/tutorials/coredns/index.html +++ b/latest/tutorials/coredns/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/coredns/... + Redirecting to ../../../v0.13.6/tutorials/coredns/... \ No newline at end of file diff --git a/latest/tutorials/designate/index.html b/latest/tutorials/designate/index.html index 49ff693455..feb9b1b137 100644 --- a/latest/tutorials/designate/index.html +++ b/latest/tutorials/designate/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/designate/... + Redirecting to ../../../v0.13.6/tutorials/designate/... \ No newline at end of file diff --git a/latest/tutorials/digitalocean/index.html b/latest/tutorials/digitalocean/index.html index a74e71b746..1a45401b4a 100644 --- a/latest/tutorials/digitalocean/index.html +++ b/latest/tutorials/digitalocean/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/digitalocean/... + Redirecting to ../../../v0.13.6/tutorials/digitalocean/... \ No newline at end of file diff --git a/latest/tutorials/dnsimple/index.html b/latest/tutorials/dnsimple/index.html index 47c4e76c19..af5c75ef14 100644 --- a/latest/tutorials/dnsimple/index.html +++ b/latest/tutorials/dnsimple/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/dnsimple/... + Redirecting to ../../../v0.13.6/tutorials/dnsimple/... \ No newline at end of file diff --git a/latest/tutorials/dyn/index.html b/latest/tutorials/dyn/index.html index 14e9fe259f..8470f63d54 100644 --- a/latest/tutorials/dyn/index.html +++ b/latest/tutorials/dyn/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/dyn/... + Redirecting to ../../../v0.13.6/tutorials/dyn/... \ No newline at end of file diff --git a/latest/tutorials/exoscale/index.html b/latest/tutorials/exoscale/index.html index 6f81c9accc..0d12be50e1 100644 --- a/latest/tutorials/exoscale/index.html +++ b/latest/tutorials/exoscale/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/exoscale/... + Redirecting to ../../../v0.13.6/tutorials/exoscale/... \ No newline at end of file diff --git a/latest/tutorials/externalname/index.html b/latest/tutorials/externalname/index.html index 18ffbc2cb7..1ab829e1be 100644 --- a/latest/tutorials/externalname/index.html +++ b/latest/tutorials/externalname/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/externalname/... + Redirecting to ../../../v0.13.6/tutorials/externalname/... \ No newline at end of file diff --git a/latest/tutorials/f5-virtualserver/index.html b/latest/tutorials/f5-virtualserver/index.html index 69e4931507..219df88a39 100644 --- a/latest/tutorials/f5-virtualserver/index.html +++ b/latest/tutorials/f5-virtualserver/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/f5-virtualserver/... + Redirecting to ../../../v0.13.6/tutorials/f5-virtualserver/... \ No newline at end of file diff --git a/latest/tutorials/gandi/index.html b/latest/tutorials/gandi/index.html index f49a01be76..777419a3dc 100644 --- a/latest/tutorials/gandi/index.html +++ b/latest/tutorials/gandi/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/gandi/... + Redirecting to ../../../v0.13.6/tutorials/gandi/... \ No newline at end of file diff --git a/latest/tutorials/gateway-api/index.html b/latest/tutorials/gateway-api/index.html index 42da2cffd0..f2dc8c5d1c 100644 --- a/latest/tutorials/gateway-api/index.html +++ b/latest/tutorials/gateway-api/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/gateway-api/... + Redirecting to ../../../v0.13.6/tutorials/gateway-api/... \ No newline at end of file diff --git a/latest/tutorials/gke/index.html b/latest/tutorials/gke/index.html index 745c888c9a..2a8c3924b9 100644 --- a/latest/tutorials/gke/index.html +++ b/latest/tutorials/gke/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/gke/... + Redirecting to ../../../v0.13.6/tutorials/gke/... \ No newline at end of file diff --git a/latest/tutorials/gloo-proxy/index.html b/latest/tutorials/gloo-proxy/index.html index 3208b7704f..c00bb2ed3f 100644 --- a/latest/tutorials/gloo-proxy/index.html +++ b/latest/tutorials/gloo-proxy/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/gloo-proxy/... + Redirecting to ../../../v0.13.6/tutorials/gloo-proxy/... \ No newline at end of file diff --git a/latest/tutorials/godaddy/index.html b/latest/tutorials/godaddy/index.html index 32408779b7..80c9666957 100644 --- a/latest/tutorials/godaddy/index.html +++ b/latest/tutorials/godaddy/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/godaddy/... + Redirecting to ../../../v0.13.6/tutorials/godaddy/... \ No newline at end of file diff --git a/latest/tutorials/hostport/index.html b/latest/tutorials/hostport/index.html index facc2bf89f..5db154e4f1 100644 --- a/latest/tutorials/hostport/index.html +++ b/latest/tutorials/hostport/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/hostport/... + Redirecting to ../../../v0.13.6/tutorials/hostport/... \ No newline at end of file diff --git a/latest/tutorials/ibmcloud/index.html b/latest/tutorials/ibmcloud/index.html index 08cb69b12c..e8c80e3c34 100644 --- a/latest/tutorials/ibmcloud/index.html +++ b/latest/tutorials/ibmcloud/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/ibmcloud/... + Redirecting to ../../../v0.13.6/tutorials/ibmcloud/... \ No newline at end of file diff --git a/latest/tutorials/infoblox/index.html b/latest/tutorials/infoblox/index.html index dc4217eecc..bc5087b51e 100644 --- a/latest/tutorials/infoblox/index.html +++ b/latest/tutorials/infoblox/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/infoblox/... + Redirecting to ../../../v0.13.6/tutorials/infoblox/... \ No newline at end of file diff --git a/latest/tutorials/istio/index.html b/latest/tutorials/istio/index.html index 0637807604..22f6da1ff4 100644 --- a/latest/tutorials/istio/index.html +++ b/latest/tutorials/istio/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/istio/... + Redirecting to ../../../v0.13.6/tutorials/istio/... \ No newline at end of file diff --git a/latest/tutorials/kong/index.html b/latest/tutorials/kong/index.html index d1a143c607..20aa556338 100644 --- a/latest/tutorials/kong/index.html +++ b/latest/tutorials/kong/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/kong/... + Redirecting to ../../../v0.13.6/tutorials/kong/... \ No newline at end of file diff --git a/latest/tutorials/kops-dns-controller/index.html b/latest/tutorials/kops-dns-controller/index.html index 97c790bdb6..df4ca3f62d 100644 --- a/latest/tutorials/kops-dns-controller/index.html +++ b/latest/tutorials/kops-dns-controller/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/kops-dns-controller/... + Redirecting to ../../../v0.13.6/tutorials/kops-dns-controller/... \ No newline at end of file diff --git a/latest/tutorials/kube-ingress-aws/index.html b/latest/tutorials/kube-ingress-aws/index.html index 11ce61c5d3..3664d97a3c 100644 --- a/latest/tutorials/kube-ingress-aws/index.html +++ b/latest/tutorials/kube-ingress-aws/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/kube-ingress-aws/... + Redirecting to ../../../v0.13.6/tutorials/kube-ingress-aws/... \ No newline at end of file diff --git a/latest/tutorials/linode/index.html b/latest/tutorials/linode/index.html index 4f04cb77c3..0a856eb007 100644 --- a/latest/tutorials/linode/index.html +++ b/latest/tutorials/linode/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/linode/... + Redirecting to ../../../v0.13.6/tutorials/linode/... \ No newline at end of file diff --git a/latest/tutorials/mx-record/index.html b/latest/tutorials/mx-record/index.html index 18cd432e31..1de951a2c4 100644 --- a/latest/tutorials/mx-record/index.html +++ b/latest/tutorials/mx-record/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/mx-record/... + Redirecting to ../../../v0.13.6/tutorials/mx-record/... \ No newline at end of file diff --git a/latest/tutorials/nginx-ingress/index.html b/latest/tutorials/nginx-ingress/index.html index bdd1738b4a..42fc1975bb 100644 --- a/latest/tutorials/nginx-ingress/index.html +++ b/latest/tutorials/nginx-ingress/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/nginx-ingress/... + Redirecting to ../../../v0.13.6/tutorials/nginx-ingress/... \ No newline at end of file diff --git a/latest/tutorials/nodes/index.html b/latest/tutorials/nodes/index.html index bf73d64148..c945a675b9 100644 --- a/latest/tutorials/nodes/index.html +++ b/latest/tutorials/nodes/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/nodes/... + Redirecting to ../../../v0.13.6/tutorials/nodes/... \ No newline at end of file diff --git a/latest/tutorials/ns-record/index.html b/latest/tutorials/ns-record/index.html index 07422bb7ac..4e11075497 100644 --- a/latest/tutorials/ns-record/index.html +++ b/latest/tutorials/ns-record/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/ns-record/... + Redirecting to ../../../v0.13.6/tutorials/ns-record/... \ No newline at end of file diff --git a/latest/tutorials/ns1/index.html b/latest/tutorials/ns1/index.html index ecf6a384cd..0205ad145d 100644 --- a/latest/tutorials/ns1/index.html +++ b/latest/tutorials/ns1/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/ns1/... + Redirecting to ../../../v0.13.6/tutorials/ns1/... \ No newline at end of file diff --git a/latest/tutorials/openshift/index.html b/latest/tutorials/openshift/index.html index bb968de1dd..042db5ec1e 100644 --- a/latest/tutorials/openshift/index.html +++ b/latest/tutorials/openshift/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/openshift/... + Redirecting to ../../../v0.13.6/tutorials/openshift/... \ No newline at end of file diff --git a/latest/tutorials/oracle/index.html b/latest/tutorials/oracle/index.html index 0797aaf8ae..4097a31cd1 100644 --- a/latest/tutorials/oracle/index.html +++ b/latest/tutorials/oracle/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/oracle/... + Redirecting to ../../../v0.13.6/tutorials/oracle/... \ No newline at end of file diff --git a/latest/tutorials/ovh/index.html b/latest/tutorials/ovh/index.html index 65de0e4a67..112576d256 100644 --- a/latest/tutorials/ovh/index.html +++ b/latest/tutorials/ovh/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/ovh/... + Redirecting to ../../../v0.13.6/tutorials/ovh/... \ No newline at end of file diff --git a/latest/tutorials/pdns/index.html b/latest/tutorials/pdns/index.html index 397cad3bd1..2c8adbba77 100644 --- a/latest/tutorials/pdns/index.html +++ b/latest/tutorials/pdns/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/pdns/... + Redirecting to ../../../v0.13.6/tutorials/pdns/... \ No newline at end of file diff --git a/latest/tutorials/pihole/index.html b/latest/tutorials/pihole/index.html index ad54c0c4f2..0094c9df89 100644 --- a/latest/tutorials/pihole/index.html +++ b/latest/tutorials/pihole/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/pihole/... + Redirecting to ../../../v0.13.6/tutorials/pihole/... \ No newline at end of file diff --git a/latest/tutorials/plural/index.html b/latest/tutorials/plural/index.html index 494f2e1add..2b4dbf05ae 100644 --- a/latest/tutorials/plural/index.html +++ b/latest/tutorials/plural/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/plural/... + Redirecting to ../../../v0.13.6/tutorials/plural/... \ No newline at end of file diff --git a/latest/tutorials/public-private-route53/index.html b/latest/tutorials/public-private-route53/index.html index 31ea891d1b..fccd4bc739 100644 --- a/latest/tutorials/public-private-route53/index.html +++ b/latest/tutorials/public-private-route53/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/public-private-route53/... + Redirecting to ../../../v0.13.6/tutorials/public-private-route53/... \ No newline at end of file diff --git a/latest/tutorials/rcodezero/index.html b/latest/tutorials/rcodezero/index.html index 701e0b7818..1bac1ec7e0 100644 --- a/latest/tutorials/rcodezero/index.html +++ b/latest/tutorials/rcodezero/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/rcodezero/... + Redirecting to ../../../v0.13.6/tutorials/rcodezero/... \ No newline at end of file diff --git a/latest/tutorials/rdns/index.html b/latest/tutorials/rdns/index.html index a9e210a768..0b78a30dec 100644 --- a/latest/tutorials/rdns/index.html +++ b/latest/tutorials/rdns/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/rdns/... + Redirecting to ../../../v0.13.6/tutorials/rdns/... \ No newline at end of file diff --git a/latest/tutorials/rfc2136/index.html b/latest/tutorials/rfc2136/index.html index eda71fa5df..5227d9024f 100644 --- a/latest/tutorials/rfc2136/index.html +++ b/latest/tutorials/rfc2136/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/rfc2136/... + Redirecting to ../../../v0.13.6/tutorials/rfc2136/... \ No newline at end of file diff --git a/latest/tutorials/scaleway/index.html b/latest/tutorials/scaleway/index.html index d0093b6d5b..9b4ee8def4 100644 --- a/latest/tutorials/scaleway/index.html +++ b/latest/tutorials/scaleway/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/scaleway/... + Redirecting to ../../../v0.13.6/tutorials/scaleway/... \ No newline at end of file diff --git a/latest/tutorials/security-context/index.html b/latest/tutorials/security-context/index.html index a84bd972c6..fd8bb1d361 100644 --- a/latest/tutorials/security-context/index.html +++ b/latest/tutorials/security-context/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/security-context/... + Redirecting to ../../../v0.13.6/tutorials/security-context/... \ No newline at end of file diff --git a/latest/tutorials/tencentcloud/index.html b/latest/tutorials/tencentcloud/index.html index 94bb270626..873696119a 100644 --- a/latest/tutorials/tencentcloud/index.html +++ b/latest/tutorials/tencentcloud/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/tencentcloud/... + Redirecting to ../../../v0.13.6/tutorials/tencentcloud/... \ No newline at end of file diff --git a/latest/tutorials/traefik-proxy/index.html b/latest/tutorials/traefik-proxy/index.html new file mode 100644 index 0000000000..fa5dec3325 --- /dev/null +++ b/latest/tutorials/traefik-proxy/index.html @@ -0,0 +1,16 @@ + + + + + Redirecting + + + + + Redirecting to ../../../v0.13.6/tutorials/traefik-proxy/... + + \ No newline at end of file diff --git a/latest/tutorials/transip/index.html b/latest/tutorials/transip/index.html index c9723ae48a..aefec5e069 100644 --- a/latest/tutorials/transip/index.html +++ b/latest/tutorials/transip/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/transip/... + Redirecting to ../../../v0.13.6/tutorials/transip/... \ No newline at end of file diff --git a/latest/tutorials/ultradns/index.html b/latest/tutorials/ultradns/index.html index 66bc82abc5..9df1aaf294 100644 --- a/latest/tutorials/ultradns/index.html +++ b/latest/tutorials/ultradns/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/ultradns/... + Redirecting to ../../../v0.13.6/tutorials/ultradns/... \ No newline at end of file diff --git a/latest/tutorials/vinyldns/index.html b/latest/tutorials/vinyldns/index.html index 62876b84aa..1779cd2412 100644 --- a/latest/tutorials/vinyldns/index.html +++ b/latest/tutorials/vinyldns/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/vinyldns/... + Redirecting to ../../../v0.13.6/tutorials/vinyldns/... \ No newline at end of file diff --git a/latest/tutorials/vultr/index.html b/latest/tutorials/vultr/index.html index 95f12c2668..b710202350 100644 --- a/latest/tutorials/vultr/index.html +++ b/latest/tutorials/vultr/index.html @@ -4,13 +4,13 @@ Redirecting - Redirecting to ../../../v0.13.5/tutorials/vultr/... + Redirecting to ../../../v0.13.6/tutorials/vultr/... \ No newline at end of file diff --git a/v0.13.6/20190708-external-dns-incubator/index.html b/v0.13.6/20190708-external-dns-incubator/index.html new file mode 100644 index 0000000000..124058daeb --- /dev/null +++ b/v0.13.6/20190708-external-dns-incubator/index.html @@ -0,0 +1,2296 @@ + + + + + + + + + + + + + + + + + + Out of Incubator - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + +
+
+ + + + + + + + +

Move ExternalDNS out of Kubernetes incubator

+ + + + + +

Summary

+

ExternalDNS is a project that synchronizes Kubernetes’ Services, Ingresses and other Kubernetes resources to DNS backends for several DNS providers.

+

The projects was started as a Kubernetes Incubator project in February 2017 and being the Kubernetes incubation initiative officially over, the maintainers want to propose the project to be moved to the kubernetes GitHub organization or to kubernetes-sigs, under the sponsorship of sig-network.

+

Motivation

+

ExternalDNS started as a community project with the goal of unifying several existing projects that were trying to solve the same problem: create DNS records for Kubernetes resources on several DNS backends.

+

When the project was proposed (see the original discussion), there were at least 3 existing implementations of the same functionality:

+ +

ExternalDNS’ goal from the beginning was to provide an officially supported solution to those problems.

+

After two years of development, the project is still in the kubernetes-sigs.

+

The incubation has been officially discontinued and to quote @thockin “Incubator projects should either become real projects in Kubernetes, shut themselves down, or move elsewhere” (see original thread here).

+

This KEP proposes to move ExternalDNS to the main Kubernetes organization or kubernetes-sigs. The “Proposal” section details the reasons behind it.

+

Goals

+

The only goal of this KEP is to establish consensus regarding the future of the ExternalDNS project and determine where it belongs.

+

Proposal

+

This KEP is about moving External DNS out of the Kubernetes incubator. This section will cover the reasons why External DNS is useful and what the community would miss in case the project would be discontinued or moved under another organization.

+

External DNS…

+
    +
  • +

    Is the de facto solution to create DNS records for several Kubernetes resources.

    +
  • +
  • +

    Is a vital component to achieve an experience close to a PaaS that many Kubernetes users try to replicate on top of Kubernetes, by allowing to automatically create DNS records for web applications.

    +
  • +
  • +

    Supports already 18 different DNS providers including all major public clouds (AWS, Azure, GCP).

    +
  • +
+

Given that the kubernetes-sigs organization will eventually be shut down, the possible alternatives to moving to be an official Kubernetes project are the following:

+
    +
  • +

    Shut down the project

    +
  • +
  • +

    Move the project elsewhere

    +
  • +
+

We believe that those alternatives would result in a worse outcome for the community compared to moving the project to the any of the other official Kubernetes organizations.
+In fact, shutting down ExternalDNS can cause:

+
    +
  • +

    The community to rebuild the same solution as already happened multiple times before the project was launched. Currently ExternalDNS is easy to be found, referenced in many articles/tutorials and for that reason not exposed to that risk.

    +
  • +
  • +

    Existing users of the projects to be left without a future proof working solution.

    +
  • +
+

Moving the ExternalDNS project outside of Kubernetes projects would cause:

+
    +
  • +

    Problems (re-)establishing user trust which could eventually lead to fragmentation and duplication.

    +
  • +
  • +

    It would be hard to establish in which organization the project should be moved to. The most natural would be Zalando’s organization, being the company that put most of the work on the project. While it is possible to assume Zalando’s commitment to open-source, that would be a strategic mistake for the project community and for the Kubernetes ecosystem due to the obvious lack of neutrality.

    +
  • +
  • +

    Lack of resources to test, lack of issue management via automation.

    +
  • +
+

For those reasons, we propose to move ExternalDNS out of the Kubernetes incubator, to live either under the kubernetes or kubernetes-sigs organization to keep being a vital part of the Kubernetes ecosystem.

+

Details

+

Graduation Criteria

+

ExternalDNS is a two years old project widely used in production by many companies. The implementation for the three major cloud providers (AWS, Azure, GCP) is stable, not changing its logic and the project is being used in production by many company using Kubernetes.

+

We have evidence that many companies are using ExternalDNS in production, but it is out of scope for this proposal to collect a comprehensive list of companies.

+

The project was quoted by a number of tutorials on the web, including the official tutorials from AWS.

+

ExternalDNS can’t be consider to be “done”: while the core functionality has been implemented, there is lack of integration testing and structural changes that are needed.

+

Those are identified in the project roadmap, which is roughly made of the following items:

+
    +
  • +

    Decoupling of the providers

    +
      +
    • +

      Implementation proposal

      +
    • +
    • +

      Development

      +
    • +
    +
  • +
  • +

    Bug fixing and performance optimization (i.e. rate limiting on cloud providers)

    +
  • +
  • +

    Integration testing suite, to be implemented at least for the “stable” providers

    +
  • +
+

For those reasons, we consider ExternalDNS to be in Beta state as a project. We believe that once the items mentioned above will be implemented, the project can reach a declared GA status.

+

There are a number of other factors that need to be covered to fully describe the state of the project, including who are the maintainers, the way we release and manage the project and so on.

+

Maintainers

+

The project has the following maintainers:

+
    +
  • +

    hjacobs

    +
  • +
  • +

    Raffo

    +
  • +
  • +

    linki

    +
  • +
  • +

    njuettner

    +
  • +
+

The list of maintainers shrunk over time as people moved out of the original development team (all the team members were working at Zalando at the time of project creation) and the project required less work.

+

The high number of providers contributed to the project pose a maintainability challenge: it is hard to bring the providers forward in terms of functionalities or even test them. The maintainers believe that the plan to transform the current Provider interface from a Go interface to an API will allow for enough decoupling and to hand over the maintenance of those plugins to the contributors themselves, see the risk and mitigations section for further details.

+

Release process, artifacts

+

The project uses the free quota of TravisCI to run tests for the project.

+

The release pipeline for the project is currently fully owned by Zalando. It runs on the internal system of the company (closed source) which external maintainers/users can’t access and that pushes images to the publicly accessible docker registry available at the URL registry.opensource.zalan.do.

+

The docker registry service is provided as best effort with no sort of SLA and the maintainers team openly suggests the users to build and maintain their own docker image based on the provided Dockerfiles.

+

Providing a vanity URL for the docker images was consider a non goal till now, but the community seems to be wanting official images from a GCR domain, similarly to what is available for other parts of official Kubernetes projects.

+

ExternalDNS does not follow a specific release cycle. Releases are made often when there are major contributions (i.e. new providers) or important bug fixes. That said, the default branch is considered stable and can be used as well to build images.

+

Risks and Mitigations

+

The following are risks that were identified:

+
    +
  • +

    Low number of maintainers: we are currently facing issues keeping up with the number of pull requests and issues giving the low number of maintainers. The list of maintainers already shrunk from 8 maintainers to 4.

    +
  • +
  • +

    Issues maintaining community contributed providers: we often lack access to external providers (i.e. InfoBlox, etc.) and this means that we cannot verify the implementations and/or run regression tests that go beyond unit testing.

    +
  • +
  • +

    Somewhat low quality of releases due to lack of integration testing.

    +
  • +
+

We think that the following actions will constitute appropriate mitigations:

+
    +
  • +

    Decoupling the providers via an API will allow us to resolve the problem of the providers. Being the project already more than 2 years old and given that there are 18 providers implemented, we possess enough information to define an API that we can be stable in a short timeframe. Once this is stable, the problem of testing the providers can be deferred to be a provider’s responsibility. This will also reduce the scope of External DNS core code, which means that there will be no need for a further increase of the maintaining team.

    +
  • +
  • +

    We added integration testing for the main cloud providers to the roadmap for the 1.0 release to make sure that we cover the mostly used ones. We believe that this item should be tackled independently from the decoupling of providers as it would be capable of generating value independently from the result of the decoupling efforts.

    +
  • +
  • +

    With the move to the Kubernetes incubation, we hope that we will be able to access the testing resources of the Kubernetes project. In this way, we hope to decouple the project from the dependency on Zalando’s internal CI tool. This will help open up the possibility to increase the visibility on the project from external contributors, which currently would be blocked by the lack of access to the software used for the whole release pipeline.

    +
  • +
+ +
+
+ + + Last update: + December 13, 2020 + + + +
+ + +
+
+
+ + + + Back to top + + +
+ + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/v0.13.6/404.html b/v0.13.6/404.html new file mode 100644 index 0000000000..bf19daa63a --- /dev/null +++ b/v0.13.6/404.html @@ -0,0 +1,1841 @@ + + + + + + + + + + + + + + + + + + external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+ +

404 - Not found

+ +
+
+
+ + + + Back to top + + +
+ + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/v0.13.6/CONTRIBUTING/index.html b/v0.13.6/CONTRIBUTING/index.html new file mode 100644 index 0000000000..c854063363 --- /dev/null +++ b/v0.13.6/CONTRIBUTING/index.html @@ -0,0 +1,2024 @@ + + + + + + + + + + + + + + + + + + Kubernetes Contributions - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + +
+
+ + + + + + + + +

Contributing Guidelines

+

Welcome to Kubernetes. We are excited about the prospect of you joining our community! The Kubernetes community abides by the CNCF code of conduct. Here is an excerpt:

+

As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.

+

Getting Started

+

We have full documentation on how to get started contributing here:

+ +

Mentorship

+
    +
  • Mentoring Initiatives - We have a diverse set of mentorship programs available that are always looking for volunteers!
  • +
+

Contact Information

+ + +
+
+ + + Last update: + September 6, 2023 + + + +
+ + +
+
+
+ + + + Back to top + + +
+ + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/v0.13.6/LICENSE/index.html b/v0.13.6/LICENSE/index.html new file mode 100644 index 0000000000..7de39dcea3 --- /dev/null +++ b/v0.13.6/LICENSE/index.html @@ -0,0 +1,2100 @@ + + + + + + + + + + + + + + + + + + License - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + +
+
+ + + + + + + + +

License

+ +
                             Apache License
+                       Version 2.0, January 2004
+                    http://www.apache.org/licenses/
+
+

TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

+
    +
  1. +

    Definitions.

    +

    “License” shall mean the terms and conditions for use, reproduction,
    + and distribution as defined by Sections 1 through 9 of this document.

    +

    “Licensor” shall mean the copyright owner or entity authorized by
    + the copyright owner that is granting the License.

    +

    “Legal Entity” shall mean the union of the acting entity and all
    + other entities that control, are controlled by, or are under common
    + control with that entity. For the purposes of this definition,
    + “control” means (i) the power, direct or indirect, to cause the
    + direction or management of such entity, whether by contract or
    + otherwise, or (ii) ownership of fifty percent (50%) or more of the
    + outstanding shares, or (iii) beneficial ownership of such entity.

    +

    “You” (or “Your”) shall mean an individual or Legal Entity
    + exercising permissions granted by this License.

    +

    “Source” form shall mean the preferred form for making modifications,
    + including but not limited to software source code, documentation
    + source, and configuration files.

    +

    “Object” form shall mean any form resulting from mechanical
    + transformation or translation of a Source form, including but
    + not limited to compiled object code, generated documentation,
    + and conversions to other media types.

    +

    “Work” shall mean the work of authorship, whether in Source or
    + Object form, made available under the License, as indicated by a
    + copyright notice that is included in or attached to the work
    + (an example is provided in the Appendix below).

    +

    “Derivative Works” shall mean any work, whether in Source or Object
    + form, that is based on (or derived from) the Work and for which the
    + editorial revisions, annotations, elaborations, or other modifications
    + represent, as a whole, an original work of authorship. For the purposes
    + of this License, Derivative Works shall not include works that remain
    + separable from, or merely link (or bind by name) to the interfaces of,
    + the Work and Derivative Works thereof.

    +

    “Contribution” shall mean any work of authorship, including
    + the original version of the Work and any modifications or additions
    + to that Work or Derivative Works thereof, that is intentionally
    + submitted to Licensor for inclusion in the Work by the copyright owner
    + or by an individual or Legal Entity authorized to submit on behalf of
    + the copyright owner. For the purposes of this definition, “submitted”
    + means any form of electronic, verbal, or written communication sent
    + to the Licensor or its representatives, including but not limited to
    + communication on electronic mailing lists, source code control systems,
    + and issue tracking systems that are managed by, or on behalf of, the
    + Licensor for the purpose of discussing and improving the Work, but
    + excluding communication that is conspicuously marked or otherwise
    + designated in writing by the copyright owner as “Not a Contribution.”

    +

    “Contributor” shall mean Licensor and any individual or Legal Entity
    + on behalf of whom a Contribution has been received by Licensor and
    + subsequently incorporated within the Work.

    +
  2. +
  3. +

    Grant of Copyright License. Subject to the terms and conditions of
    + this License, each Contributor hereby grants to You a perpetual,
    + worldwide, non-exclusive, no-charge, royalty-free, irrevocable
    + copyright license to reproduce, prepare Derivative Works of,
    + publicly display, publicly perform, sublicense, and distribute the
    + Work and such Derivative Works in Source or Object form.

    +
  4. +
  5. +

    Grant of Patent License. Subject to the terms and conditions of
    + this License, each Contributor hereby grants to You a perpetual,
    + worldwide, non-exclusive, no-charge, royalty-free, irrevocable
    + (except as stated in this section) patent license to make, have made,
    + use, offer to sell, sell, import, and otherwise transfer the Work,
    + where such license applies only to those patent claims licensable
    + by such Contributor that are necessarily infringed by their
    + Contribution(s) alone or by combination of their Contribution(s)
    + with the Work to which such Contribution(s) was submitted. If You
    + institute patent litigation against any entity (including a
    + cross-claim or counterclaim in a lawsuit) alleging that the Work
    + or a Contribution incorporated within the Work constitutes direct
    + or contributory patent infringement, then any patent licenses
    + granted to You under this License for that Work shall terminate
    + as of the date such litigation is filed.

    +
  6. +
  7. +

    Redistribution. You may reproduce and distribute copies of the
    + Work or Derivative Works thereof in any medium, with or without
    + modifications, and in Source or Object form, provided that You
    + meet the following conditions:

    +

    (a) You must give any other recipients of the Work or
    + Derivative Works a copy of this License; and

    +

    (b) You must cause any modified files to carry prominent notices
    + stating that You changed the files; and

    +

    © You must retain, in the Source form of any Derivative Works
    + that You distribute, all copyright, patent, trademark, and
    + attribution notices from the Source form of the Work,
    + excluding those notices that do not pertain to any part of
    + the Derivative Works; and

    +

    (d) If the Work includes a “NOTICE” text file as part of its
    + distribution, then any Derivative Works that You distribute must
    + include a readable copy of the attribution notices contained
    + within such NOTICE file, excluding those notices that do not
    + pertain to any part of the Derivative Works, in at least one
    + of the following places: within a NOTICE text file distributed
    + as part of the Derivative Works; within the Source form or
    + documentation, if provided along with the Derivative Works; or,
    + within a display generated by the Derivative Works, if and
    + wherever such third-party notices normally appear. The contents
    + of the NOTICE file are for informational purposes only and
    + do not modify the License. You may add Your own attribution
    + notices within Derivative Works that You distribute, alongside
    + or as an addendum to the NOTICE text from the Work, provided
    + that such additional attribution notices cannot be construed
    + as modifying the License.

    +

    You may add Your own copyright statement to Your modifications and
    + may provide additional or different license terms and conditions
    + for use, reproduction, or distribution of Your modifications, or
    + for any such Derivative Works as a whole, provided Your use,
    + reproduction, and distribution of the Work otherwise complies with
    + the conditions stated in this License.

    +
  8. +
  9. +

    Submission of Contributions. Unless You explicitly state otherwise,
    + any Contribution intentionally submitted for inclusion in the Work
    + by You to the Licensor shall be under the terms and conditions of
    + this License, without any additional terms or conditions.
    + Notwithstanding the above, nothing herein shall supersede or modify
    + the terms of any separate license agreement you may have executed
    + with Licensor regarding such Contributions.

    +
  10. +
  11. +

    Trademarks. This License does not grant permission to use the trade
    + names, trademarks, service marks, or product names of the Licensor,
    + except as required for reasonable and customary use in describing the
    + origin of the Work and reproducing the content of the NOTICE file.

    +
  12. +
  13. +

    Disclaimer of Warranty. Unless required by applicable law or
    + agreed to in writing, Licensor provides the Work (and each
    + Contributor provides its Contributions) on an “AS IS” BASIS,
    + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
    + implied, including, without limitation, any warranties or conditions
    + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
    + PARTICULAR PURPOSE. You are solely responsible for determining the
    + appropriateness of using or redistributing the Work and assume any
    + risks associated with Your exercise of permissions under this License.

    +
  14. +
  15. +

    Limitation of Liability. In no event and under no legal theory,
    + whether in tort (including negligence), contract, or otherwise,
    + unless required by applicable law (such as deliberate and grossly
    + negligent acts) or agreed to in writing, shall any Contributor be
    + liable to You for damages, including any direct, indirect, special,
    + incidental, or consequential damages of any character arising as a
    + result of this License or out of the use or inability to use the
    + Work (including but not limited to damages for loss of goodwill,
    + work stoppage, computer failure or malfunction, or any and all
    + other commercial damages or losses), even if such Contributor
    + has been advised of the possibility of such damages.

    +
  16. +
  17. +

    Accepting Warranty or Additional Liability. While redistributing
    + the Work or Derivative Works thereof, You may choose to offer,
    + and charge a fee for, acceptance of support, warranty, indemnity,
    + or other liability obligations and/or rights consistent with this
    + License. However, in accepting such obligations, You may act only
    + on Your own behalf and on Your sole responsibility, not on behalf
    + of any other Contributor, and only if You agree to indemnify,
    + defend, and hold each Contributor harmless for any liability
    + incurred by, or claims asserted against, such Contributor by reason
    + of your accepting any such warranty or additional liability.

    +
  18. +
+

END OF TERMS AND CONDITIONS

+

APPENDIX: How to apply the Apache License to your work.

+
  To apply the Apache License to your work, attach the following
+  boilerplate notice, with the fields enclosed by brackets "{}"
+  replaced with your own identifying information. (Don't include
+  the brackets!)  The text should be enclosed in the appropriate
+  comment syntax for the file format. We also recommend that a
+  file or class name and description of purpose be included on the
+  same "printed page" as the copyright notice for easier
+  identification within third-party archives.
+
+

Copyright {yyyy} {name of copyright owner}

+

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.

+ +
+
+ + + Last update: + September 6, 2023 + + + +
+ + +
+
+
+ + + + Back to top + + +
+ + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/v0.13.6/annotations/annotations/index.html b/v0.13.6/annotations/annotations/index.html new file mode 100644 index 0000000000..a719f5af90 --- /dev/null +++ b/v0.13.6/annotations/annotations/index.html @@ -0,0 +1,2386 @@ + + + + + + + + + + + + + + + + + + About - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + + + +
+ + + + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + +
+
+
+ + + + + + +
+
+ + + + + + + + +

Annotations

+

ExternalDNS sources support a number of annotations on the Kubernetes resources that they examine.

+

The following table documents which sources support which annotations:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Sourcecontrollerhostnameinternal-hostnametargetttl(provider-specific)
AmbassadorYes
Connector
ContourYesYes1YesYesYes
CloudFoundry
CRD
F5Yes
GatewayYesYes1YesYes
GlooYesYes
IngressYesYes1YesYesYes
IstioYesYes1YesYesYes
KongYesYesYes
NodeYesYes
OpenShiftYesYes1YesYesYes
PodYesYes
ServiceYesYes1Yes12Yes3YesYes
SkipperYesYes1YesYesYes
TraefikYesYesYesYes
+

external-dns.alpha.kubernetes.io/access

+

Specifies which set of node IP addresses to use for a Service of type NodePort.

+

If the value is public, use the Nodes’ addresses of type ExternalIP, plus IPv6 addresses of type InternalIP.

+

If the value is private, use the Nodes’ addresses of type InternalIP.

+

If the annotation is not present and there is at least one address of type ExternalIP,
+behave as if the value were public, otherwise behave as if the value were private.

+

external-dns.alpha.kubernetes.io/controller

+

If this annotation exists and has a value other than dns-controller then the source ignores the resource.

+

external-dns.alpha.kubernetes.io/endpoints-type

+

Specifies which set of addresses to use for a headless Service.

+

If the value is NodeExternalIP, use each relevant Pod’s Node’s address of type ExternalIP
+plus each IPv6 address of type InternalIP.

+

Otherwise, if the value is HostIP or the --publish-host-ip flag is specified, use
+each relevant Pod’s Status.HostIP.

+

Otherwise, use the IP of each of the Service’s Endpoints’s Addresses.

+

external-dns.alpha.kubernetes.io/hostname

+

Specifies the domain for the resource’s DNS records.

+

external-dns.alpha.kubernetes.io/ingress-hostname-source

+

Specifies where to get the domain for an Ingress resource.

+

If the value is defined-hosts-only, use only the domains from the Ingress spec.

+

If the value is annotation-only, use only the domains from the Ingress annotations.

+

If the annotation is not present, use the domains from both the spec and annotations.

+

external-dns.alpha.kubernetes.io/internal-hostname

+

Specifies the domain for the resource’s DNS records that are for use from internal networks.

+

For Services of type LoadBalancer, uses the Service’s ClusterIP.

+

For Pods, uses the Pod’s Status.PodIP.

+

external-dns.alpha.kubernetes.io/target

+

Specifies a comma-separated list of values to override the resource’s DNS record targets (RDATA).

+

Targets that parse as IPv4 addresses are published as A records and
+targets that parse as IPv6 addresses are published as AAAA records. All other targets
+are published as CNAME records.

+

external-dns.alpha.kubernetes.io/ttl

+

Specifies the TTL (time to live) for the resource’s DNS records.

+

The value may be specified as either a duration or an integer number of seconds.
+It must be between 1 and 2,147,483,647 seconds.

+

Provider-specific annotations

+

Some providers define their own annotations. Cloud-specific annotations have keys prefixed as follows:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
CloudAnnotation prefix
AWSexternal-dns.alpha.kubernetes.io/aws-
CloudFlareexternal-dns.alpha.kubernetes.io/cloudflare-
IBM Cloudexternal-dns.alpha.kubernetes.io/ibmcloud-
Scalewayexternal-dns.alpha.kubernetes.io/scw-
+

Additional annotations that are currently implemented only by AWS are:

+

external-dns.alpha.kubernetes.io/alias

+

If the value of this annotation is true, specifies that CNAME records generated by the
+resource should instead be alias records.

+

This annotation is only relevant if the --aws-prefer-cname flag is specified.

+

external-dns.alpha.kubernetes.io/set-identifier

+

Specifies the set identifier for DNS records generated by the resource.

+

A set identifier differentiates among multiple DNS record sets that have the same combination of domain and type.
+Which record set or sets are returned to queries is then determined by the configured routing policy.

+
+
+
    +
  1. +

    Unless the --ignore-hostname-annotation flag is specified. 

    +
  2. +
  3. +

    Only behaves differently than hostname for Services of type LoadBalancer

    +
  4. +
  5. +

    Also supported on Pods referenced from a headless Service’s Endpoints

    +
  6. +
+
+ +
+
+ + + Last update: + June 25, 2023 + + + +
+ + +
+
+
+ + + + Back to top + + +
+ + + +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/v0.13.6/assets/images/favicon.png b/v0.13.6/assets/images/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..1cf13b9f9d978896599290a74f77d5dbe7d1655c GIT binary patch literal 1870 zcmV-U2eJ5xP)Gc)JR9QMau)O=X#!i9;T z37kk-upj^(fsR36MHs_+1RCI)NNu9}lD0S{B^g8PN?Ww(5|~L#Ng*g{WsqleV}|#l zz8@ri&cTzw_h33bHI+12+kK6WN$h#n5cD8OQt`5kw6p~9H3()bUQ8OS4Q4HTQ=1Ol z_JAocz`fLbT2^{`8n~UAo=#AUOf=SOq4pYkt;XbC&f#7lb$*7=$na!mWCQ`dBQsO0 zLFBSPj*N?#u5&pf2t4XjEGH|=pPQ8xh7tpx;US5Cx_Ju;!O`ya-yF`)b%TEt5>eP1ZX~}sjjA%FJF?h7cX8=b!DZl<6%Cv z*G0uvvU+vmnpLZ2paivG-(cd*y3$hCIcsZcYOGh{$&)A6*XX&kXZd3G8m)G$Zz-LV z^GF3VAW^Mdv!)4OM8EgqRiz~*Cji;uzl2uC9^=8I84vNp;ltJ|q-*uQwGp2ma6cY7 z;`%`!9UXO@fr&Ebapfs34OmS9^u6$)bJxrucutf>`dKPKT%%*d3XlFVKunp9 zasduxjrjs>f8V=D|J=XNZp;_Zy^WgQ$9WDjgY=z@stwiEBm9u5*|34&1Na8BMjjgf3+SHcr`5~>oz1Y?SW^=K z^bTyO6>Gar#P_W2gEMwq)ot3; zREHn~U&Dp0l6YT0&k-wLwYjb?5zGK`W6S2v+K>AM(95m2C20L|3m~rN8dprPr@t)5lsk9Hu*W z?pS990s;Ez=+Rj{x7p``4>+c0G5^pYnB1^!TL=(?HLHZ+HicG{~4F1d^5Awl_2!1jICM-!9eoLhbbT^;yHcefyTAaqRcY zmuctDopPT!%k+}x%lZRKnzykr2}}XfG_ne?nRQO~?%hkzo;@RN{P6o`&mMUWBYMTe z6i8ChtjX&gXl`nvrU>jah)2iNM%JdjqoaeaU%yVn!^70x-flljp6Q5tK}5}&X8&&G zX3fpb3E(!rH=zVI_9Gjl45w@{(ITqngWFe7@9{mX;tO25Z_8 zQHEpI+FkTU#4xu>RkN>b3Tnc3UpWzPXWm#o55GKF09j^Mh~)K7{QqbO_~(@CVq! zS<8954|P8mXN2MRs86xZ&Q4EfM@JB94b=(YGuk)s&^jiSF=t3*oNK3`rD{H`yQ?d; ztE=laAUoZx5?RC8*WKOj`%LXEkgDd>&^Q4M^z`%u0rg-It=hLCVsq!Z%^6eB-OvOT zFZ28TN&cRmgU}Elrnk43)!>Z1FCPL2K$7}gwzIc48NX}#!A1BpJP?#v5wkNprhV** z?Cpalt1oH&{r!o3eSKc&ap)iz2BTn_VV`4>9M^b3;(YY}4>#ML6{~(4mH+?%07*qo IM6N<$f(jP3KmY&$ literal 0 HcmV?d00001 diff --git a/v0.13.6/assets/javascripts/bundle.c44cc438.min.js b/v0.13.6/assets/javascripts/bundle.c44cc438.min.js new file mode 100644 index 0000000000..6602556849 --- /dev/null +++ b/v0.13.6/assets/javascripts/bundle.c44cc438.min.js @@ -0,0 +1,29 @@ +(()=>{var ta=Object.create;var xr=Object.defineProperty;var ra=Object.getOwnPropertyDescriptor;var na=Object.getOwnPropertyNames,Rt=Object.getOwnPropertySymbols,oa=Object.getPrototypeOf,Sr=Object.prototype.hasOwnProperty,an=Object.prototype.propertyIsEnumerable;var on=(e,t,r)=>t in e?xr(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,$=(e,t)=>{for(var r in t||(t={}))Sr.call(t,r)&&on(e,r,t[r]);if(Rt)for(var r of Rt(t))an.call(t,r)&&on(e,r,t[r]);return e};var sn=(e,t)=>{var r={};for(var n in e)Sr.call(e,n)&&t.indexOf(n)<0&&(r[n]=e[n]);if(e!=null&&Rt)for(var n of Rt(e))t.indexOf(n)<0&&an.call(e,n)&&(r[n]=e[n]);return r};var vt=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var ia=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of na(t))!Sr.call(e,o)&&o!==r&&xr(e,o,{get:()=>t[o],enumerable:!(n=ra(t,o))||n.enumerable});return e};var Ke=(e,t,r)=>(r=e!=null?ta(oa(e)):{},ia(t||!e||!e.__esModule?xr(r,"default",{value:e,enumerable:!0}):r,e));var un=vt((wr,cn)=>{(function(e,t){typeof wr=="object"&&typeof cn!="undefined"?t():typeof define=="function"&&define.amd?define(t):t()})(wr,function(){"use strict";function e(r){var n=!0,o=!1,i=null,a={text:!0,search:!0,url:!0,tel:!0,email:!0,password:!0,number:!0,date:!0,month:!0,week:!0,time:!0,datetime:!0,"datetime-local":!0};function s(_){return!!(_&&_!==document&&_.nodeName!=="HTML"&&_.nodeName!=="BODY"&&"classList"in _&&"contains"in _.classList)}function c(_){var je=_.type,he=_.tagName;return!!(he==="INPUT"&&a[je]&&!_.readOnly||he==="TEXTAREA"&&!_.readOnly||_.isContentEditable)}function u(_){_.classList.contains("focus-visible")||(_.classList.add("focus-visible"),_.setAttribute("data-focus-visible-added",""))}function f(_){!_.hasAttribute("data-focus-visible-added")||(_.classList.remove("focus-visible"),_.removeAttribute("data-focus-visible-added"))}function p(_){_.metaKey||_.altKey||_.ctrlKey||(s(r.activeElement)&&u(r.activeElement),n=!0)}function l(_){n=!1}function d(_){!s(_.target)||(n||c(_.target))&&u(_.target)}function h(_){!s(_.target)||(_.target.classList.contains("focus-visible")||_.target.hasAttribute("data-focus-visible-added"))&&(o=!0,window.clearTimeout(i),i=window.setTimeout(function(){o=!1},100),f(_.target))}function b(_){document.visibilityState==="hidden"&&(o&&(n=!0),F())}function F(){document.addEventListener("mousemove",U),document.addEventListener("mousedown",U),document.addEventListener("mouseup",U),document.addEventListener("pointermove",U),document.addEventListener("pointerdown",U),document.addEventListener("pointerup",U),document.addEventListener("touchmove",U),document.addEventListener("touchstart",U),document.addEventListener("touchend",U)}function K(){document.removeEventListener("mousemove",U),document.removeEventListener("mousedown",U),document.removeEventListener("mouseup",U),document.removeEventListener("pointermove",U),document.removeEventListener("pointerdown",U),document.removeEventListener("pointerup",U),document.removeEventListener("touchmove",U),document.removeEventListener("touchstart",U),document.removeEventListener("touchend",U)}function U(_){_.target.nodeName&&_.target.nodeName.toLowerCase()==="html"||(n=!1,K())}document.addEventListener("keydown",p,!0),document.addEventListener("mousedown",l,!0),document.addEventListener("pointerdown",l,!0),document.addEventListener("touchstart",l,!0),document.addEventListener("visibilitychange",b,!0),F(),r.addEventListener("focus",d,!0),r.addEventListener("blur",h,!0),r.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&r.host?r.host.setAttribute("data-js-focus-visible",""):r.nodeType===Node.DOCUMENT_NODE&&(document.documentElement.classList.add("js-focus-visible"),document.documentElement.setAttribute("data-js-focus-visible",""))}if(typeof window!="undefined"&&typeof document!="undefined"){window.applyFocusVisiblePolyfill=e;var t;try{t=new CustomEvent("focus-visible-polyfill-ready")}catch(r){t=document.createEvent("CustomEvent"),t.initCustomEvent("focus-visible-polyfill-ready",!1,!1,{})}window.dispatchEvent(t)}typeof document!="undefined"&&e(document)})});var fn=vt(Er=>{(function(e){var t=function(){try{return!!Symbol.iterator}catch(u){return!1}},r=t(),n=function(u){var f={next:function(){var p=u.shift();return{done:p===void 0,value:p}}};return r&&(f[Symbol.iterator]=function(){return f}),f},o=function(u){return encodeURIComponent(u).replace(/%20/g,"+")},i=function(u){return decodeURIComponent(String(u).replace(/\+/g," "))},a=function(){var u=function(p){Object.defineProperty(this,"_entries",{writable:!0,value:{}});var l=typeof p;if(l!=="undefined")if(l==="string")p!==""&&this._fromString(p);else if(p instanceof u){var d=this;p.forEach(function(K,U){d.append(U,K)})}else if(p!==null&&l==="object")if(Object.prototype.toString.call(p)==="[object Array]")for(var h=0;hd[0]?1:0}),u._entries&&(u._entries={});for(var p=0;p1?i(d[1]):"")}})})(typeof global!="undefined"?global:typeof window!="undefined"?window:typeof self!="undefined"?self:Er);(function(e){var t=function(){try{var o=new e.URL("b","http://a");return o.pathname="c d",o.href==="http://a/c%20d"&&o.searchParams}catch(i){return!1}},r=function(){var o=e.URL,i=function(c,u){typeof c!="string"&&(c=String(c)),u&&typeof u!="string"&&(u=String(u));var f=document,p;if(u&&(e.location===void 0||u!==e.location.href)){u=u.toLowerCase(),f=document.implementation.createHTMLDocument(""),p=f.createElement("base"),p.href=u,f.head.appendChild(p);try{if(p.href.indexOf(u)!==0)throw new Error(p.href)}catch(_){throw new Error("URL unable to set base "+u+" due to "+_)}}var l=f.createElement("a");l.href=c,p&&(f.body.appendChild(l),l.href=l.href);var d=f.createElement("input");if(d.type="url",d.value=c,l.protocol===":"||!/:/.test(l.href)||!d.checkValidity()&&!u)throw new TypeError("Invalid URL");Object.defineProperty(this,"_anchorElement",{value:l});var h=new e.URLSearchParams(this.search),b=!0,F=!0,K=this;["append","delete","set"].forEach(function(_){var je=h[_];h[_]=function(){je.apply(h,arguments),b&&(F=!1,K.search=h.toString(),F=!0)}}),Object.defineProperty(this,"searchParams",{value:h,enumerable:!0});var U=void 0;Object.defineProperty(this,"_updateSearchParams",{enumerable:!1,configurable:!1,writable:!1,value:function(){this.search!==U&&(U=this.search,F&&(b=!1,this.searchParams._fromString(this.search),b=!0))}})},a=i.prototype,s=function(c){Object.defineProperty(a,c,{get:function(){return this._anchorElement[c]},set:function(u){this._anchorElement[c]=u},enumerable:!0})};["hash","host","hostname","port","protocol"].forEach(function(c){s(c)}),Object.defineProperty(a,"search",{get:function(){return this._anchorElement.search},set:function(c){this._anchorElement.search=c,this._updateSearchParams()},enumerable:!0}),Object.defineProperties(a,{toString:{get:function(){var c=this;return function(){return c.href}}},href:{get:function(){return this._anchorElement.href.replace(/\?$/,"")},set:function(c){this._anchorElement.href=c,this._updateSearchParams()},enumerable:!0},pathname:{get:function(){return this._anchorElement.pathname.replace(/(^\/?)/,"/")},set:function(c){this._anchorElement.pathname=c},enumerable:!0},origin:{get:function(){var c={"http:":80,"https:":443,"ftp:":21}[this._anchorElement.protocol],u=this._anchorElement.port!=c&&this._anchorElement.port!=="";return this._anchorElement.protocol+"//"+this._anchorElement.hostname+(u?":"+this._anchorElement.port:"")},enumerable:!0},password:{get:function(){return""},set:function(c){},enumerable:!0},username:{get:function(){return""},set:function(c){},enumerable:!0}}),i.createObjectURL=function(c){return o.createObjectURL.apply(o,arguments)},i.revokeObjectURL=function(c){return o.revokeObjectURL.apply(o,arguments)},e.URL=i};if(t()||r(),e.location!==void 0&&!("origin"in e.location)){var n=function(){return e.location.protocol+"//"+e.location.hostname+(e.location.port?":"+e.location.port:"")};try{Object.defineProperty(e.location,"origin",{get:n,enumerable:!0})}catch(o){setInterval(function(){e.location.origin=n()},100)}}})(typeof global!="undefined"?global:typeof window!="undefined"?window:typeof self!="undefined"?self:Er)});var Rn=vt((Us,Pt)=>{/*! ***************************************************************************** +Copyright (c) Microsoft Corporation. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +***************************************************************************** */var pn,ln,mn,dn,hn,bn,vn,gn,yn,kt,Or,xn,Sn,wn,rt,En,On,_n,Tn,Mn,Ln,An,Cn,Ht;(function(e){var t=typeof global=="object"?global:typeof self=="object"?self:typeof this=="object"?this:{};typeof define=="function"&&define.amd?define("tslib",["exports"],function(n){e(r(t,r(n)))}):typeof Pt=="object"&&typeof Pt.exports=="object"?e(r(t,r(Pt.exports))):e(r(t));function r(n,o){return n!==t&&(typeof Object.create=="function"?Object.defineProperty(n,"__esModule",{value:!0}):n.__esModule=!0),function(i,a){return n[i]=o?o(i,a):a}}})(function(e){var t=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(n,o){n.__proto__=o}||function(n,o){for(var i in o)Object.prototype.hasOwnProperty.call(o,i)&&(n[i]=o[i])};pn=function(n,o){if(typeof o!="function"&&o!==null)throw new TypeError("Class extends value "+String(o)+" is not a constructor or null");t(n,o);function i(){this.constructor=n}n.prototype=o===null?Object.create(o):(i.prototype=o.prototype,new i)},ln=Object.assign||function(n){for(var o,i=1,a=arguments.length;i=0;f--)(u=n[f])&&(c=(s<3?u(c):s>3?u(o,i,c):u(o,i))||c);return s>3&&c&&Object.defineProperty(o,i,c),c},hn=function(n,o){return function(i,a){o(i,a,n)}},bn=function(n,o){if(typeof Reflect=="object"&&typeof Reflect.metadata=="function")return Reflect.metadata(n,o)},vn=function(n,o,i,a){function s(c){return c instanceof i?c:new i(function(u){u(c)})}return new(i||(i=Promise))(function(c,u){function f(d){try{l(a.next(d))}catch(h){u(h)}}function p(d){try{l(a.throw(d))}catch(h){u(h)}}function l(d){d.done?c(d.value):s(d.value).then(f,p)}l((a=a.apply(n,o||[])).next())})},gn=function(n,o){var i={label:0,sent:function(){if(c[0]&1)throw c[1];return c[1]},trys:[],ops:[]},a,s,c,u;return u={next:f(0),throw:f(1),return:f(2)},typeof Symbol=="function"&&(u[Symbol.iterator]=function(){return this}),u;function f(l){return function(d){return p([l,d])}}function p(l){if(a)throw new TypeError("Generator is already executing.");for(;i;)try{if(a=1,s&&(c=l[0]&2?s.return:l[0]?s.throw||((c=s.return)&&c.call(s),0):s.next)&&!(c=c.call(s,l[1])).done)return c;switch(s=0,c&&(l=[l[0]&2,c.value]),l[0]){case 0:case 1:c=l;break;case 4:return i.label++,{value:l[1],done:!1};case 5:i.label++,s=l[1],l=[0];continue;case 7:l=i.ops.pop(),i.trys.pop();continue;default:if(c=i.trys,!(c=c.length>0&&c[c.length-1])&&(l[0]===6||l[0]===2)){i=0;continue}if(l[0]===3&&(!c||l[1]>c[0]&&l[1]=n.length&&(n=void 0),{value:n&&n[a++],done:!n}}};throw new TypeError(o?"Object is not iterable.":"Symbol.iterator is not defined.")},Or=function(n,o){var i=typeof Symbol=="function"&&n[Symbol.iterator];if(!i)return n;var a=i.call(n),s,c=[],u;try{for(;(o===void 0||o-- >0)&&!(s=a.next()).done;)c.push(s.value)}catch(f){u={error:f}}finally{try{s&&!s.done&&(i=a.return)&&i.call(a)}finally{if(u)throw u.error}}return c},xn=function(){for(var n=[],o=0;o1||f(b,F)})})}function f(b,F){try{p(a[b](F))}catch(K){h(c[0][3],K)}}function p(b){b.value instanceof rt?Promise.resolve(b.value.v).then(l,d):h(c[0][2],b)}function l(b){f("next",b)}function d(b){f("throw",b)}function h(b,F){b(F),c.shift(),c.length&&f(c[0][0],c[0][1])}},On=function(n){var o,i;return o={},a("next"),a("throw",function(s){throw s}),a("return"),o[Symbol.iterator]=function(){return this},o;function a(s,c){o[s]=n[s]?function(u){return(i=!i)?{value:rt(n[s](u)),done:s==="return"}:c?c(u):u}:c}},_n=function(n){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var o=n[Symbol.asyncIterator],i;return o?o.call(n):(n=typeof kt=="function"?kt(n):n[Symbol.iterator](),i={},a("next"),a("throw"),a("return"),i[Symbol.asyncIterator]=function(){return this},i);function a(c){i[c]=n[c]&&function(u){return new Promise(function(f,p){u=n[c](u),s(f,p,u.done,u.value)})}}function s(c,u,f,p){Promise.resolve(p).then(function(l){c({value:l,done:f})},u)}},Tn=function(n,o){return Object.defineProperty?Object.defineProperty(n,"raw",{value:o}):n.raw=o,n};var r=Object.create?function(n,o){Object.defineProperty(n,"default",{enumerable:!0,value:o})}:function(n,o){n.default=o};Mn=function(n){if(n&&n.__esModule)return n;var o={};if(n!=null)for(var i in n)i!=="default"&&Object.prototype.hasOwnProperty.call(n,i)&&Ht(o,n,i);return r(o,n),o},Ln=function(n){return n&&n.__esModule?n:{default:n}},An=function(n,o,i,a){if(i==="a"&&!a)throw new TypeError("Private accessor was defined without a getter");if(typeof o=="function"?n!==o||!a:!o.has(n))throw new TypeError("Cannot read private member from an object whose class did not declare it");return i==="m"?a:i==="a"?a.call(n):a?a.value:o.get(n)},Cn=function(n,o,i,a,s){if(a==="m")throw new TypeError("Private method is not writable");if(a==="a"&&!s)throw new TypeError("Private accessor was defined without a setter");if(typeof o=="function"?n!==o||!s:!o.has(n))throw new TypeError("Cannot write private member to an object whose class did not declare it");return a==="a"?s.call(n,i):s?s.value=i:o.set(n,i),i},e("__extends",pn),e("__assign",ln),e("__rest",mn),e("__decorate",dn),e("__param",hn),e("__metadata",bn),e("__awaiter",vn),e("__generator",gn),e("__exportStar",yn),e("__createBinding",Ht),e("__values",kt),e("__read",Or),e("__spread",xn),e("__spreadArrays",Sn),e("__spreadArray",wn),e("__await",rt),e("__asyncGenerator",En),e("__asyncDelegator",On),e("__asyncValues",_n),e("__makeTemplateObject",Tn),e("__importStar",Mn),e("__importDefault",Ln),e("__classPrivateFieldGet",An),e("__classPrivateFieldSet",Cn)})});var Yr=vt((Mt,Kr)=>{/*! + * clipboard.js v2.0.10 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */(function(t,r){typeof Mt=="object"&&typeof Kr=="object"?Kr.exports=r():typeof define=="function"&&define.amd?define([],r):typeof Mt=="object"?Mt.ClipboardJS=r():t.ClipboardJS=r()})(Mt,function(){return function(){var e={686:function(n,o,i){"use strict";i.d(o,{default:function(){return ea}});var a=i(279),s=i.n(a),c=i(370),u=i.n(c),f=i(817),p=i.n(f);function l(I){try{return document.execCommand(I)}catch(M){return!1}}var d=function(M){var w=p()(M);return l("cut"),w},h=d;function b(I){var M=document.documentElement.getAttribute("dir")==="rtl",w=document.createElement("textarea");w.style.fontSize="12pt",w.style.border="0",w.style.padding="0",w.style.margin="0",w.style.position="absolute",w.style[M?"right":"left"]="-9999px";var W=window.pageYOffset||document.documentElement.scrollTop;return w.style.top="".concat(W,"px"),w.setAttribute("readonly",""),w.value=I,w}var F=function(M){var w=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},W="";if(typeof M=="string"){var R=b(M);w.container.appendChild(R),W=p()(R),l("copy"),R.remove()}else W=p()(M),l("copy");return W},K=F;function U(I){return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?U=function(w){return typeof w}:U=function(w){return w&&typeof Symbol=="function"&&w.constructor===Symbol&&w!==Symbol.prototype?"symbol":typeof w},U(I)}var _=function(){var M=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},w=M.action,W=w===void 0?"copy":w,R=M.container,N=M.target,Oe=M.text;if(W!=="copy"&&W!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(N!==void 0)if(N&&U(N)==="object"&&N.nodeType===1){if(W==="copy"&&N.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(W==="cut"&&(N.hasAttribute("readonly")||N.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`)}else throw new Error('Invalid "target" value, use a valid Element');if(Oe)return K(Oe,{container:R});if(N)return W==="cut"?h(N):K(N,{container:R})},je=_;function he(I){return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?he=function(w){return typeof w}:he=function(w){return w&&typeof Symbol=="function"&&w.constructor===Symbol&&w!==Symbol.prototype?"symbol":typeof w},he(I)}function tt(I,M){if(!(I instanceof M))throw new TypeError("Cannot call a class as a function")}function nn(I,M){for(var w=0;w0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof R.action=="function"?R.action:this.defaultAction,this.target=typeof R.target=="function"?R.target:this.defaultTarget,this.text=typeof R.text=="function"?R.text:this.defaultText,this.container=he(R.container)==="object"?R.container:document.body}},{key:"listenClick",value:function(R){var N=this;this.listener=u()(R,"click",function(Oe){return N.onClick(Oe)})}},{key:"onClick",value:function(R){var N=R.delegateTarget||R.currentTarget,Oe=this.action(N)||"copy",Ct=je({action:Oe,container:this.container,target:this.target(N),text:this.text(N)});this.emit(Ct?"success":"error",{action:Oe,text:Ct,trigger:N,clearSelection:function(){N&&N.focus(),document.activeElement.blur(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(R){return yr("action",R)}},{key:"defaultTarget",value:function(R){var N=yr("target",R);if(N)return document.querySelector(N)}},{key:"defaultText",value:function(R){return yr("text",R)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(R){var N=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return K(R,N)}},{key:"cut",value:function(R){return h(R)}},{key:"isSupported",value:function(){var R=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],N=typeof R=="string"?[R]:R,Oe=!!document.queryCommandSupported;return N.forEach(function(Ct){Oe=Oe&&!!document.queryCommandSupported(Ct)}),Oe}}]),w}(s()),ea=Zi},828:function(n){var o=9;if(typeof Element!="undefined"&&!Element.prototype.matches){var i=Element.prototype;i.matches=i.matchesSelector||i.mozMatchesSelector||i.msMatchesSelector||i.oMatchesSelector||i.webkitMatchesSelector}function a(s,c){for(;s&&s.nodeType!==o;){if(typeof s.matches=="function"&&s.matches(c))return s;s=s.parentNode}}n.exports=a},438:function(n,o,i){var a=i(828);function s(f,p,l,d,h){var b=u.apply(this,arguments);return f.addEventListener(l,b,h),{destroy:function(){f.removeEventListener(l,b,h)}}}function c(f,p,l,d,h){return typeof f.addEventListener=="function"?s.apply(null,arguments):typeof l=="function"?s.bind(null,document).apply(null,arguments):(typeof f=="string"&&(f=document.querySelectorAll(f)),Array.prototype.map.call(f,function(b){return s(b,p,l,d,h)}))}function u(f,p,l,d){return function(h){h.delegateTarget=a(h.target,p),h.delegateTarget&&d.call(f,h)}}n.exports=c},879:function(n,o){o.node=function(i){return i!==void 0&&i instanceof HTMLElement&&i.nodeType===1},o.nodeList=function(i){var a=Object.prototype.toString.call(i);return i!==void 0&&(a==="[object NodeList]"||a==="[object HTMLCollection]")&&"length"in i&&(i.length===0||o.node(i[0]))},o.string=function(i){return typeof i=="string"||i instanceof String},o.fn=function(i){var a=Object.prototype.toString.call(i);return a==="[object Function]"}},370:function(n,o,i){var a=i(879),s=i(438);function c(l,d,h){if(!l&&!d&&!h)throw new Error("Missing required arguments");if(!a.string(d))throw new TypeError("Second argument must be a String");if(!a.fn(h))throw new TypeError("Third argument must be a Function");if(a.node(l))return u(l,d,h);if(a.nodeList(l))return f(l,d,h);if(a.string(l))return p(l,d,h);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function u(l,d,h){return l.addEventListener(d,h),{destroy:function(){l.removeEventListener(d,h)}}}function f(l,d,h){return Array.prototype.forEach.call(l,function(b){b.addEventListener(d,h)}),{destroy:function(){Array.prototype.forEach.call(l,function(b){b.removeEventListener(d,h)})}}}function p(l,d,h){return s(document.body,l,d,h)}n.exports=c},817:function(n){function o(i){var a;if(i.nodeName==="SELECT")i.focus(),a=i.value;else if(i.nodeName==="INPUT"||i.nodeName==="TEXTAREA"){var s=i.hasAttribute("readonly");s||i.setAttribute("readonly",""),i.select(),i.setSelectionRange(0,i.value.length),s||i.removeAttribute("readonly"),a=i.value}else{i.hasAttribute("contenteditable")&&i.focus();var c=window.getSelection(),u=document.createRange();u.selectNodeContents(i),c.removeAllRanges(),c.addRange(u),a=c.toString()}return a}n.exports=o},279:function(n){function o(){}o.prototype={on:function(i,a,s){var c=this.e||(this.e={});return(c[i]||(c[i]=[])).push({fn:a,ctx:s}),this},once:function(i,a,s){var c=this;function u(){c.off(i,u),a.apply(s,arguments)}return u._=a,this.on(i,u,s)},emit:function(i){var a=[].slice.call(arguments,1),s=((this.e||(this.e={}))[i]||[]).slice(),c=0,u=s.length;for(c;c{"use strict";/*! + * escape-html + * Copyright(c) 2012-2013 TJ Holowaychuk + * Copyright(c) 2015 Andreas Lubbe + * Copyright(c) 2015 Tiancheng "Timothy" Gu + * MIT Licensed + */var xs=/["'&<>]/;yi.exports=Ss;function Ss(e){var t=""+e,r=xs.exec(t);if(!r)return t;var n,o="",i=0,a=0;for(i=r.index;i0},enumerable:!1,configurable:!0}),t.prototype._trySubscribe=function(r){return this._throwIfClosed(),e.prototype._trySubscribe.call(this,r)},t.prototype._subscribe=function(r){return this._throwIfClosed(),this._checkFinalizedStatuses(r),this._innerSubscribe(r)},t.prototype._innerSubscribe=function(r){var n=this,o=this,i=o.hasError,a=o.isStopped,s=o.observers;return i||a?_r:(this.currentObservers=null,s.push(r),new Ae(function(){n.currentObservers=null,ke(s,r)}))},t.prototype._checkFinalizedStatuses=function(r){var n=this,o=n.hasError,i=n.thrownError,a=n.isStopped;o?r.error(i):a&&r.complete()},t.prototype.asObservable=function(){var r=new k;return r.source=this,r},t.create=function(r,n){return new zn(r,n)},t}(k);var zn=function(e){te(t,e);function t(r,n){var o=e.call(this)||this;return o.destination=r,o.source=n,o}return t.prototype.next=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.next)===null||o===void 0||o.call(n,r)},t.prototype.error=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.error)===null||o===void 0||o.call(n,r)},t.prototype.complete=function(){var r,n;(n=(r=this.destination)===null||r===void 0?void 0:r.complete)===null||n===void 0||n.call(r)},t.prototype._subscribe=function(r){var n,o;return(o=(n=this.source)===null||n===void 0?void 0:n.subscribe(r))!==null&&o!==void 0?o:_r},t}(O);var yt={now:function(){return(yt.delegate||Date).now()},delegate:void 0};var xt=function(e){te(t,e);function t(r,n,o){r===void 0&&(r=1/0),n===void 0&&(n=1/0),o===void 0&&(o=yt);var i=e.call(this)||this;return i._bufferSize=r,i._windowTime=n,i._timestampProvider=o,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=n===1/0,i._bufferSize=Math.max(1,r),i._windowTime=Math.max(1,n),i}return t.prototype.next=function(r){var n=this,o=n.isStopped,i=n._buffer,a=n._infiniteTimeWindow,s=n._timestampProvider,c=n._windowTime;o||(i.push(r),!a&&i.push(s.now()+c)),this._trimBuffer(),e.prototype.next.call(this,r)},t.prototype._subscribe=function(r){this._throwIfClosed(),this._trimBuffer();for(var n=this._innerSubscribe(r),o=this,i=o._infiniteTimeWindow,a=o._buffer,s=a.slice(),c=0;c0?e.prototype.requestAsyncId.call(this,r,n,o):(r.actions.push(this),r._scheduled||(r._scheduled=st.requestAnimationFrame(function(){return r.flush(void 0)})))},t.prototype.recycleAsyncId=function(r,n,o){if(o===void 0&&(o=0),o!=null&&o>0||o==null&&this.delay>0)return e.prototype.recycleAsyncId.call(this,r,n,o);r.actions.some(function(i){return i.id===n})||(st.cancelAnimationFrame(n),r._scheduled=void 0)},t}(Vt);var Kn=function(e){te(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.flush=function(r){this._active=!0;var n=this._scheduled;this._scheduled=void 0;var o=this.actions,i;r=r||o.shift();do if(i=r.execute(r.state,r.delay))break;while((r=o[0])&&r.id===n&&o.shift());if(this._active=!1,i){for(;(r=o[0])&&r.id===n&&o.shift();)r.unsubscribe();throw i}},t}(Nt);var Te=new Kn(Qn);var z=new k(function(e){return e.complete()});function zt(e){return e&&E(e.schedule)}function kr(e){return e[e.length-1]}function Fe(e){return E(kr(e))?e.pop():void 0}function ye(e){return zt(kr(e))?e.pop():void 0}function qt(e,t){return typeof kr(e)=="number"?e.pop():t}var ct=function(e){return e&&typeof e.length=="number"&&typeof e!="function"};function Qt(e){return E(e==null?void 0:e.then)}function Kt(e){return E(e[at])}function Yt(e){return Symbol.asyncIterator&&E(e==null?void 0:e[Symbol.asyncIterator])}function Bt(e){return new TypeError("You provided "+(e!==null&&typeof e=="object"?"an invalid object":"'"+e+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function da(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var Gt=da();function Jt(e){return E(e==null?void 0:e[Gt])}function Xt(e){return Pn(this,arguments,function(){var r,n,o,i;return It(this,function(a){switch(a.label){case 0:r=e.getReader(),a.label=1;case 1:a.trys.push([1,,9,10]),a.label=2;case 2:return[4,$t(r.read())];case 3:return n=a.sent(),o=n.value,i=n.done,i?[4,$t(void 0)]:[3,5];case 4:return[2,a.sent()];case 5:return[4,$t(o)];case 6:return[4,a.sent()];case 7:return a.sent(),[3,2];case 8:return[3,10];case 9:return r.releaseLock(),[7];case 10:return[2]}})})}function Zt(e){return E(e==null?void 0:e.getReader)}function V(e){if(e instanceof k)return e;if(e!=null){if(Kt(e))return ha(e);if(ct(e))return ba(e);if(Qt(e))return va(e);if(Yt(e))return Yn(e);if(Jt(e))return ga(e);if(Zt(e))return ya(e)}throw Bt(e)}function ha(e){return new k(function(t){var r=e[at]();if(E(r.subscribe))return r.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function ba(e){return new k(function(t){for(var r=0;r=2,!0))}function ae(e){e===void 0&&(e={});var t=e.connector,r=t===void 0?function(){return new O}:t,n=e.resetOnError,o=n===void 0?!0:n,i=e.resetOnComplete,a=i===void 0?!0:i,s=e.resetOnRefCountZero,c=s===void 0?!0:s;return function(u){var f=null,p=null,l=null,d=0,h=!1,b=!1,F=function(){p==null||p.unsubscribe(),p=null},K=function(){F(),f=l=null,h=b=!1},U=function(){var _=f;K(),_==null||_.unsubscribe()};return v(function(_,je){d++,!b&&!h&&F();var he=l=l!=null?l:r();je.add(function(){d--,d===0&&!b&&!h&&(p=Ur(U,c))}),he.subscribe(je),f||(f=new it({next:function(tt){return he.next(tt)},error:function(tt){b=!0,F(),p=Ur(K,o,tt),he.error(tt)},complete:function(){h=!0,F(),p=Ur(K,a),he.complete()}}),ne(_).subscribe(f))})(u)}}function Ur(e,t){for(var r=[],n=2;ne.next(document)),e}function G(e,t=document){return Array.from(t.querySelectorAll(e))}function Q(e,t=document){let r=ue(e,t);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${e}" to be present`);return r}function ue(e,t=document){return t.querySelector(e)||void 0}function Ne(){return document.activeElement instanceof HTMLElement&&document.activeElement||void 0}function nr(e){return C(y(document.body,"focusin"),y(document.body,"focusout")).pipe(Xe(1),m(()=>{let t=Ne();return typeof t!="undefined"?e.contains(t):!1}),q(e===Ne()),Y())}function ze(e){return{x:e.offsetLeft,y:e.offsetTop}}function ho(e){return C(y(window,"load"),y(window,"resize")).pipe(He(0,Te),m(()=>ze(e)),q(ze(e)))}function bo(e){return{x:e.scrollLeft,y:e.scrollTop}}function or(e){return C(y(e,"scroll"),y(window,"resize")).pipe(He(0,Te),m(()=>bo(e)),q(bo(e)))}var go=function(){if(typeof Map!="undefined")return Map;function e(t,r){var n=-1;return t.some(function(o,i){return o[0]===r?(n=i,!0):!1}),n}return function(){function t(){this.__entries__=[]}return Object.defineProperty(t.prototype,"size",{get:function(){return this.__entries__.length},enumerable:!0,configurable:!0}),t.prototype.get=function(r){var n=e(this.__entries__,r),o=this.__entries__[n];return o&&o[1]},t.prototype.set=function(r,n){var o=e(this.__entries__,r);~o?this.__entries__[o][1]=n:this.__entries__.push([r,n])},t.prototype.delete=function(r){var n=this.__entries__,o=e(n,r);~o&&n.splice(o,1)},t.prototype.has=function(r){return!!~e(this.__entries__,r)},t.prototype.clear=function(){this.__entries__.splice(0)},t.prototype.forEach=function(r,n){n===void 0&&(n=null);for(var o=0,i=this.__entries__;o0},e.prototype.connect_=function(){!zr||this.connected_||(document.addEventListener("transitionend",this.onTransitionEnd_),window.addEventListener("resize",this.refresh),Da?(this.mutationsObserver_=new MutationObserver(this.refresh),this.mutationsObserver_.observe(document,{attributes:!0,childList:!0,characterData:!0,subtree:!0})):(document.addEventListener("DOMSubtreeModified",this.refresh),this.mutationEventsAdded_=!0),this.connected_=!0)},e.prototype.disconnect_=function(){!zr||!this.connected_||(document.removeEventListener("transitionend",this.onTransitionEnd_),window.removeEventListener("resize",this.refresh),this.mutationsObserver_&&this.mutationsObserver_.disconnect(),this.mutationEventsAdded_&&document.removeEventListener("DOMSubtreeModified",this.refresh),this.mutationsObserver_=null,this.mutationEventsAdded_=!1,this.connected_=!1)},e.prototype.onTransitionEnd_=function(t){var r=t.propertyName,n=r===void 0?"":r,o=Wa.some(function(i){return!!~n.indexOf(i)});o&&this.refresh()},e.getInstance=function(){return this.instance_||(this.instance_=new e),this.instance_},e.instance_=null,e}(),yo=function(e,t){for(var r=0,n=Object.keys(t);r0},e}(),So=typeof WeakMap!="undefined"?new WeakMap:new go,wo=function(){function e(t){if(!(this instanceof e))throw new TypeError("Cannot call a class as a function.");if(!arguments.length)throw new TypeError("1 argument required, but only 0 present.");var r=Va.getInstance(),n=new Xa(t,r,this);So.set(this,n)}return e}();["observe","unobserve","disconnect"].forEach(function(e){wo.prototype[e]=function(){var t;return(t=So.get(this))[e].apply(t,arguments)}});var Za=function(){return typeof ir.ResizeObserver!="undefined"?ir.ResizeObserver:wo}(),Eo=Za;var Oo=new O,es=P(()=>H(new Eo(e=>{for(let t of e)Oo.next(t)}))).pipe(x(e=>C(xe,H(e)).pipe(L(()=>e.disconnect()))),X(1));function Ce(e){return{width:e.offsetWidth,height:e.offsetHeight}}function ge(e){return es.pipe(S(t=>t.observe(e)),x(t=>Oo.pipe(T(({target:r})=>r===e),L(()=>t.unobserve(e)),m(()=>Ce(e)))),q(Ce(e)))}function cr(e){return{width:e.scrollWidth,height:e.scrollHeight}}var _o=new O,ts=P(()=>H(new IntersectionObserver(e=>{for(let t of e)_o.next(t)},{threshold:0}))).pipe(x(e=>C(xe,H(e)).pipe(L(()=>e.disconnect()))),X(1));function To(e){return ts.pipe(S(t=>t.observe(e)),x(t=>_o.pipe(T(({target:r})=>r===e),L(()=>t.unobserve(e)),m(({isIntersecting:r})=>r))))}function Mo(e,t=16){return or(e).pipe(m(({y:r})=>{let n=Ce(e),o=cr(e);return r>=o.height-n.height-t}),Y())}var ur={drawer:Q("[data-md-toggle=drawer]"),search:Q("[data-md-toggle=search]")};function Lo(e){return ur[e].checked}function qe(e,t){ur[e].checked!==t&&ur[e].click()}function mt(e){let t=ur[e];return y(t,"change").pipe(m(()=>t.checked),q(t.checked))}function rs(e,t){switch(e.constructor){case HTMLInputElement:return e.type==="radio"?/^Arrow/.test(t):!0;case HTMLSelectElement:case HTMLTextAreaElement:return!0;default:return e.isContentEditable}}function Ao(){return y(window,"keydown").pipe(T(e=>!(e.metaKey||e.ctrlKey)),m(e=>({mode:Lo("search")?"search":"global",type:e.key,claim(){e.preventDefault(),e.stopPropagation()}})),T(({mode:e,type:t})=>{if(e==="global"){let r=Ne();if(typeof r!="undefined")return!rs(r,t)}return!0}),ae())}function Se(){return new URL(location.href)}function fr(e){location.href=e.href}function Co(){return new O}function Ro(e,t){if(typeof t=="string"||typeof t=="number")e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(let r of t)Ro(e,r)}function A(e,t,...r){let n=document.createElement(e);if(t)for(let o of Object.keys(t))typeof t[o]!="boolean"?n.setAttribute(o,t[o]):t[o]&&n.setAttribute(o,"");for(let o of r)Ro(n,o);return n}function ko(e,t){let r=t;if(e.length>r){for(;e[r]!==" "&&--r>0;);return`${e.substring(0,r)}...`}return e}function pr(e){if(e>999){let t=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(t)}k`}else return e.toString()}function Ho(){return location.hash.substring(1)}function Po(e){let t=A("a",{href:e});t.addEventListener("click",r=>r.stopPropagation()),t.click()}function ns(){return y(window,"hashchange").pipe(m(Ho),q(Ho()),T(e=>e.length>0),X(1))}function Io(){return ns().pipe(m(e=>ue(`[id="${e}"]`)),T(e=>typeof e!="undefined"))}function qr(e){let t=matchMedia(e);return tr(r=>t.addListener(()=>r(t.matches))).pipe(q(t.matches))}function $o(){let e=matchMedia("print");return C(y(window,"beforeprint").pipe(Z(!0)),y(window,"afterprint").pipe(Z(!1))).pipe(q(e.matches))}function Qr(e,t){return e.pipe(x(r=>r?t():z))}function lr(e,t={credentials:"same-origin"}){return ne(fetch(`${e}`,t)).pipe(T(r=>r.status===200),De(()=>z))}function Re(e,t){return lr(e,t).pipe(x(r=>r.json()),X(1))}function jo(e,t){let r=new DOMParser;return lr(e,t).pipe(x(n=>n.text()),m(n=>r.parseFromString(n,"text/xml")),X(1))}function Fo(e){let t=A("script",{src:e});return P(()=>(document.head.appendChild(t),C(y(t,"load"),y(t,"error").pipe(x(()=>Hr(()=>new ReferenceError(`Invalid script: ${e}`))))).pipe(Z(void 0),L(()=>document.head.removeChild(t)),re(1))))}function Uo(){return{x:Math.max(0,scrollX),y:Math.max(0,scrollY)}}function Wo(){return C(y(window,"scroll",{passive:!0}),y(window,"resize",{passive:!0})).pipe(m(Uo),q(Uo()))}function Do(){return{width:innerWidth,height:innerHeight}}function Vo(){return y(window,"resize",{passive:!0}).pipe(m(Do),q(Do()))}function No(){return B([Wo(),Vo()]).pipe(m(([e,t])=>({offset:e,size:t})),X(1))}function mr(e,{viewport$:t,header$:r}){let n=t.pipe(J("size")),o=B([n,r]).pipe(m(()=>ze(e)));return B([r,t,o]).pipe(m(([{height:i},{offset:a,size:s},{x:c,y:u}])=>({offset:{x:a.x-c,y:a.y-u+i},size:s})))}function zo(e,{tx$:t}){let r=y(e,"message").pipe(m(({data:n})=>n));return t.pipe(Tt(()=>r,{leading:!0,trailing:!0}),S(n=>e.postMessage(n)),_t(r),ae())}var os=Q("#__config"),dt=JSON.parse(os.textContent);dt.base=`${new URL(dt.base,Se())}`;function de(){return dt}function ce(e){return dt.features.includes(e)}function ee(e,t){return typeof t!="undefined"?dt.translations[e].replace("#",t.toString()):dt.translations[e]}function we(e,t=document){return Q(`[data-md-component=${e}]`,t)}function oe(e,t=document){return G(`[data-md-component=${e}]`,t)}var ti=Ke(Yr());function qo(e){return A("aside",{class:"md-annotation",tabIndex:0},A("div",{class:"md-annotation__inner md-tooltip"},A("div",{class:"md-tooltip__inner md-typeset"})),A("span",{class:"md-annotation__index"},A("span",{"data-md-annotation-id":e})))}function Qo(e){return A("button",{class:"md-clipboard md-icon",title:ee("clipboard.copy"),"data-clipboard-target":`#${e} > code`})}function Br(e,t){let r=t&2,n=t&1,o=Object.keys(e.terms).filter(a=>!e.terms[a]).reduce((a,s)=>[...a,A("del",null,s)," "],[]).slice(0,-1),i=new URL(e.location);return ce("search.highlight")&&i.searchParams.set("h",Object.entries(e.terms).filter(([,a])=>a).reduce((a,[s])=>`${a} ${s}`.trim(),"")),A("a",{href:`${i}`,class:"md-search-result__link",tabIndex:-1},A("article",{class:["md-search-result__article",...r?["md-search-result__article--document"]:[]].join(" "),"data-md-score":e.score.toFixed(2)},r>0&&A("div",{class:"md-search-result__icon md-icon"}),A("h1",{class:"md-search-result__title"},e.title),n>0&&e.text.length>0&&A("p",{class:"md-search-result__teaser"},ko(e.text,320)),e.tags&&e.tags.map(a=>A("span",{class:"md-tag"},a)),n>0&&o.length>0&&A("p",{class:"md-search-result__terms"},ee("search.result.term.missing"),": ",o)))}function Ko(e){let t=e[0].score,r=[...e],n=r.findIndex(u=>!u.location.includes("#")),[o]=r.splice(n,1),i=r.findIndex(u=>u.scoreBr(u,1)),...s.length?[A("details",{class:"md-search-result__more"},A("summary",{tabIndex:-1},s.length>0&&s.length===1?ee("search.result.more.one"):ee("search.result.more.other",s.length)),s.map(u=>Br(u,1)))]:[]];return A("li",{class:"md-search-result__item"},c)}function Yo(e){return A("ul",{class:"md-source__facts"},Object.entries(e).map(([t,r])=>A("li",{class:`md-source__fact md-source__fact--${t}`},typeof r=="number"?pr(r):r)))}function Bo(e){return A("div",{class:"md-typeset__scrollwrap"},A("div",{class:"md-typeset__table"},e))}function is(e){let t=de(),r=new URL(`../${e.version}/`,t.base);return A("li",{class:"md-version__item"},A("a",{href:r.toString(),class:"md-version__link"},e.title))}function Go(e,t){return A("div",{class:"md-version"},A("button",{class:"md-version__current","aria-label":ee("select.version.title")},t.title),A("ul",{class:"md-version__list"},e.map(is)))}function as(e,t){let r=P(()=>B([ho(e),or(t)])).pipe(m(([{x:n,y:o},i])=>{let{width:a}=Ce(e);return{x:n-i.x+a/2,y:o-i.y}}));return nr(e).pipe(x(n=>r.pipe(m(o=>({active:n,offset:o})),re(+!n||1/0))))}function Jo(e,t){return P(()=>{let r=new O;r.subscribe({next({offset:i}){e.style.setProperty("--md-tooltip-x",`${i.x}px`),e.style.setProperty("--md-tooltip-y",`${i.y}px`)},complete(){e.style.removeProperty("--md-tooltip-x"),e.style.removeProperty("--md-tooltip-y")}}),r.pipe(Vr(500,Te),m(()=>t.getBoundingClientRect()),m(({x:i})=>i)).subscribe({next(i){i?e.style.setProperty("--md-tooltip-0",`${-i}px`):e.style.removeProperty("--md-tooltip-0")},complete(){e.style.removeProperty("--md-tooltip-0")}});let n=Q(":scope > :last-child",e),o=y(n,"mousedown",{once:!0});return r.pipe(x(({active:i})=>i?o:z),S(i=>i.preventDefault())).subscribe(()=>e.blur()),as(e,t).pipe(S(i=>r.next(i)),L(()=>r.complete()),m(i=>$({ref:e},i)))})}function ss(e){let t=[];for(let r of G(".c, .c1, .cm",e)){let n,o=r.firstChild;if(o instanceof Text)for(;n=/\((\d+)\)/.exec(o.textContent);){let i=o.splitText(n.index);o=i.splitText(n[0].length),t.push(i)}}return t}function Xo(e,t){t.append(...Array.from(e.childNodes))}function Zo(e,t,{print$:r}){let n=new Map;for(let o of ss(t)){let[,i]=o.textContent.match(/\((\d+)\)/);ue(`li:nth-child(${i})`,e)&&(n.set(+i,qo(+i)),o.replaceWith(n.get(+i)))}return n.size===0?z:P(()=>{let o=new O;return r.pipe(se(o.pipe(pe(1)))).subscribe(i=>{e.hidden=!i;for(let[a,s]of n){let c=Q(".md-typeset",s),u=Q(`li:nth-child(${a})`,e);i?Xo(c,u):Xo(u,c)}}),C(...[...n].map(([,i])=>Jo(i,t))).pipe(L(()=>o.complete()),ae())})}var cs=0;function ri(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return ri(t)}}function ei(e){return ge(e).pipe(m(({width:t})=>({scrollable:cr(e).width>t})),J("scrollable"))}function ni(e,t){let{matches:r}=matchMedia("(hover)"),n=P(()=>{let o=new O;if(o.subscribe(({scrollable:a})=>{a&&r?e.setAttribute("tabindex","0"):e.removeAttribute("tabindex")}),ti.default.isSupported()){let a=e.closest("pre");a.id=`__code_${++cs}`,a.insertBefore(Qo(a.id),e)}let i=e.closest([":not(td):not(.code) > .highlight",".highlighttable"].join(", "));if(i instanceof HTMLElement){let a=ri(i);if(typeof a!="undefined"&&(i.classList.contains("annotate")||ce("content.code.annotate"))){let s=Zo(a,e,t);return ei(e).pipe(S(c=>o.next(c)),L(()=>o.complete()),m(c=>$({ref:e},c)),Ze(ge(i).pipe(se(o.pipe(pe(1))),m(({width:c,height:u})=>c&&u),Y(),x(c=>c?s:z))))}}return ei(e).pipe(S(a=>o.next(a)),L(()=>o.complete()),m(a=>$({ref:e},a)))});return To(e).pipe(T(o=>o),re(1),x(()=>n))}var oi=".node circle,.node ellipse,.node path,.node polygon,.node rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}marker{fill:var(--md-mermaid-edge-color)!important}.edgeLabel .label rect{fill:transparent}.label{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.label foreignObject{line-height:normal;overflow:visible}.label div .edgeLabel{color:var(--md-mermaid-label-fg-color)}.edgeLabel,.edgeLabel rect,.label div .edgeLabel{background-color:var(--md-mermaid-label-bg-color)}.edgeLabel,.edgeLabel rect{fill:var(--md-mermaid-label-bg-color);color:var(--md-mermaid-edge-color)}.edgePath .path,.flowchart-link{stroke:var(--md-mermaid-edge-color)}.edgePath .arrowheadPath{fill:var(--md-mermaid-edge-color);stroke:none}.cluster rect{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}.cluster span{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}#flowchart-circleEnd,#flowchart-circleStart,#flowchart-crossEnd,#flowchart-crossStart,#flowchart-pointEnd,#flowchart-pointStart{stroke:none}g.classGroup line,g.classGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.classGroup text{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.classLabel .box{fill:var(--md-mermaid-label-bg-color);background-color:var(--md-mermaid-label-bg-color);opacity:1}.classLabel .label{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.node .divider{stroke:var(--md-mermaid-node-fg-color)}.relation{stroke:var(--md-mermaid-edge-color)}.cardinality{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.cardinality text{fill:inherit!important}#compositionEnd,#compositionStart,#dependencyEnd,#dependencyStart,#extensionEnd,#extensionStart{fill:var(--md-mermaid-edge-color)!important;stroke:var(--md-mermaid-edge-color)!important}#aggregationEnd,#aggregationStart{fill:var(--md-mermaid-label-bg-color)!important;stroke:var(--md-mermaid-edge-color)!important}g.stateGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.stateGroup .state-title{fill:var(--md-mermaid-label-fg-color)!important;font-family:var(--md-mermaid-font-family)}g.stateGroup .composit{fill:var(--md-mermaid-label-bg-color)}.nodeLabel{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.node circle.state-end,.node circle.state-start,.start-state{fill:var(--md-mermaid-edge-color);stroke:none}.end-state-inner,.end-state-outer{fill:var(--md-mermaid-edge-color)}.end-state-inner,.node circle.state-end{stroke:var(--md-mermaid-label-bg-color)}.transition{stroke:var(--md-mermaid-edge-color)}[id^=state-fork] rect,[id^=state-join] rect{fill:var(--md-mermaid-edge-color)!important;stroke:none!important}.statediagram-cluster.statediagram-cluster .inner{fill:var(--md-default-bg-color)}.statediagram-cluster rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}.statediagram-state rect.divider{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}.entityBox{fill:var(--md-mermaid-label-bg-color);stroke:var(--md-mermaid-node-fg-color)}.entityLabel{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.relationshipLabelBox{fill:var(--md-mermaid-label-bg-color);fill-opacity:1;background-color:var(--md-mermaid-label-bg-color);opacity:1}.relationshipLabel{fill:var(--md-mermaid-label-fg-color)}.relationshipLine{stroke:var(--md-mermaid-edge-color)}#ONE_OR_MORE_END *,#ONE_OR_MORE_START *,#ONLY_ONE_END *,#ONLY_ONE_START *,#ZERO_OR_MORE_END *,#ZERO_OR_MORE_START *,#ZERO_OR_ONE_END *,#ZERO_OR_ONE_START *{stroke:var(--md-mermaid-edge-color)!important}#ZERO_OR_MORE_END circle,#ZERO_OR_MORE_START circle,.actor{fill:var(--md-mermaid-label-bg-color)}.actor{stroke:var(--md-mermaid-node-fg-color)}text.actor>tspan{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}line{stroke:var(--md-default-fg-color--lighter)}.messageLine0,.messageLine1{stroke:var(--md-mermaid-edge-color)}.loopText>tspan,.messageText{font-family:var(--md-mermaid-font-family)!important}#arrowhead path,.loopText>tspan,.messageText{fill:var(--md-mermaid-edge-color);stroke:none}.loopLine{stroke:var(--md-mermaid-node-fg-color)}.labelBox,.loopLine{fill:var(--md-mermaid-node-bg-color)}.labelBox{stroke:none}.labelText,.labelText>span{fill:var(--md-mermaid-node-fg-color);font-family:var(--md-mermaid-font-family)}";var Gr,fs=0;function ps(){return typeof mermaid=="undefined"||mermaid instanceof Element?Fo("https://unpkg.com/mermaid@8.13.3/dist/mermaid.min.js"):H(void 0)}function ii(e){return e.classList.remove("mermaid"),Gr||(Gr=ps().pipe(S(()=>mermaid.initialize({startOnLoad:!1,themeCSS:oi})),Z(void 0),X(1))),Gr.subscribe(()=>{e.classList.add("mermaid");let t=`__mermaid_${fs++}`,r=A("div",{class:"mermaid"});mermaid.mermaidAPI.render(t,e.textContent,n=>{let o=r.attachShadow({mode:"closed"});o.innerHTML=n,e.replaceWith(r)})}),Gr.pipe(Z({ref:e}))}function ls(e,{target$:t,print$:r}){let n=!0;return C(t.pipe(m(o=>o.closest("details:not([open])")),T(o=>e===o),Z({action:"open",reveal:!0})),r.pipe(T(o=>o||!n),S(()=>n=e.open),m(o=>({action:o?"open":"close"}))))}function ai(e,t){return P(()=>{let r=new O;return r.subscribe(({action:n,reveal:o})=>{n==="open"?e.setAttribute("open",""):e.removeAttribute("open"),o&&e.scrollIntoView()}),ls(e,t).pipe(S(n=>r.next(n)),L(()=>r.complete()),m(n=>$({ref:e},n)))})}var si=A("table");function ci(e){return e.replaceWith(si),si.replaceWith(Bo(e)),H({ref:e})}function ms(e){let t=G(":scope > input",e),r=t.find(n=>n.checked)||t[0];return C(...t.map(n=>y(n,"change").pipe(Z({active:Q(`label[for=${n.id}]`)})))).pipe(q({active:Q(`label[for=${r.id}]`)}))}function ui(e){let t=Q(".tabbed-labels",e);return P(()=>{let r=new O;return B([r,ge(e)]).pipe(He(1,Te),se(r.pipe(pe(1)))).subscribe({next([{active:n}]){let o=ze(n),{width:i}=Ce(n);e.style.setProperty("--md-indicator-x",`${o.x}px`),e.style.setProperty("--md-indicator-width",`${i}px`),t.scrollTo({behavior:"smooth",left:o.x})},complete(){e.style.removeProperty("--md-indicator-x"),e.style.removeProperty("--md-indicator-width")}}),ms(e).pipe(S(n=>r.next(n)),L(()=>r.complete()),m(n=>$({ref:e},n)))}).pipe(Ge(le))}function fi(e,{target$:t,print$:r}){return C(...G("pre:not(.mermaid) > code",e).map(n=>ni(n,{print$:r})),...G("pre.mermaid",e).map(n=>ii(n)),...G("table:not([class])",e).map(n=>ci(n)),...G("details",e).map(n=>ai(n,{target$:t,print$:r})),...G("[data-tabs]",e).map(n=>ui(n)))}function ds(e,{alert$:t}){return t.pipe(x(r=>C(H(!0),H(!1).pipe(Ie(2e3))).pipe(m(n=>({message:r,active:n})))))}function pi(e,t){let r=Q(".md-typeset",e);return P(()=>{let n=new O;return n.subscribe(({message:o,active:i})=>{r.textContent=o,i?e.setAttribute("data-md-state","open"):e.removeAttribute("data-md-state")}),ds(e,t).pipe(S(o=>n.next(o)),L(()=>n.complete()),m(o=>$({ref:e},o)))})}function hs({viewport$:e}){if(!ce("header.autohide"))return H(!1);let t=e.pipe(m(({offset:{y:o}})=>o),Me(2,1),m(([o,i])=>[oMath.abs(i-o.y)>100),m(([,[o]])=>o),Y()),n=mt("search");return B([e,n]).pipe(m(([{offset:o},i])=>o.y>400&&!i),Y(),x(o=>o?r:H(!1)),q(!1))}function li(e,t){return P(()=>{let r=getComputedStyle(e);return H(r.position==="sticky"||r.position==="-webkit-sticky")}).pipe(Ve(ge(e),hs(t)),m(([r,{height:n},o])=>({height:r?n:0,sticky:r,hidden:o})),Y((r,n)=>r.sticky===n.sticky&&r.height===n.height&&r.hidden===n.hidden),X(1))}function mi(e,{header$:t,main$:r}){return P(()=>{let n=new O;return n.pipe(J("active"),Ve(t)).subscribe(([{active:o},{hidden:i}])=>{o?e.setAttribute("data-md-state",i?"hidden":"shadow"):e.removeAttribute("data-md-state")}),r.subscribe(n),t.pipe(se(n.pipe(pe(1))),m(o=>$({ref:e},o)))})}function bs(e,{viewport$:t,header$:r}){return mr(e,{viewport$:t,header$:r}).pipe(m(({offset:{y:n}})=>{let{height:o}=Ce(e);return{active:n>=o}}),J("active"))}function di(e,t){return P(()=>{let r=new O;r.subscribe(({active:o})=>{o?e.setAttribute("data-md-state","active"):e.removeAttribute("data-md-state")});let n=ue("article h1");return typeof n=="undefined"?z:bs(n,t).pipe(S(o=>r.next(o)),L(()=>r.complete()),m(o=>$({ref:e},o)))})}function hi(e,{viewport$:t,header$:r}){let n=r.pipe(m(({height:i})=>i),Y()),o=n.pipe(x(()=>ge(e).pipe(m(({height:i})=>({top:e.offsetTop,bottom:e.offsetTop+i})),J("bottom"))));return B([n,o,t]).pipe(m(([i,{top:a,bottom:s},{offset:{y:c},size:{height:u}}])=>(u=Math.max(0,u-Math.max(0,a-c,i)-Math.max(0,u+c-s)),{offset:a-i,height:u,active:a-i<=c})),Y((i,a)=>i.offset===a.offset&&i.height===a.height&&i.active===a.active))}function vs(e){let t=__md_get("__palette")||{index:e.findIndex(r=>matchMedia(r.getAttribute("data-md-color-media")).matches)};return H(...e).pipe(ie(r=>y(r,"change").pipe(Z(r))),q(e[Math.max(0,t.index)]),m(r=>({index:e.indexOf(r),color:{scheme:r.getAttribute("data-md-color-scheme"),primary:r.getAttribute("data-md-color-primary"),accent:r.getAttribute("data-md-color-accent")}})),X(1))}function bi(e){return P(()=>{let t=new O;t.subscribe(n=>{for(let[o,i]of Object.entries(n.color))document.body.setAttribute(`data-md-color-${o}`,i);for(let o=0;ot.next(n)),L(()=>t.complete()),m(n=>$({ref:e},n)))})}var Jr=Ke(Yr());function gs(e){e.setAttribute("data-md-copying","");let t=e.innerText;return e.removeAttribute("data-md-copying"),t}function vi({alert$:e}){Jr.default.isSupported()&&new k(t=>{new Jr.default("[data-clipboard-target], [data-clipboard-text]",{text:r=>r.getAttribute("data-clipboard-text")||gs(Q(r.getAttribute("data-clipboard-target")))}).on("success",r=>t.next(r))}).pipe(S(t=>{t.trigger.focus()}),Z(ee("clipboard.copied"))).subscribe(e)}function ys(e){if(e.length<2)return[""];let[t,r]=[...e].sort((o,i)=>o.length-i.length).map(o=>o.replace(/[^/]+$/,"")),n=0;if(t===r)n=t.length;else for(;t.charCodeAt(n)===r.charCodeAt(n);)n++;return e.map(o=>o.replace(t.slice(0,n),""))}function dr(e){let t=__md_get("__sitemap",sessionStorage,e);if(t)return H(t);{let r=de();return jo(new URL("sitemap.xml",e||r.base)).pipe(m(n=>ys(G("loc",n).map(o=>o.textContent))),Pe([]),S(n=>__md_set("__sitemap",n,sessionStorage,e)))}}function gi({document$:e,location$:t,viewport$:r}){let n=de();if(location.protocol==="file:")return;"scrollRestoration"in history&&(history.scrollRestoration="manual",y(window,"beforeunload").subscribe(()=>{history.scrollRestoration="auto"}));let o=ue("link[rel=icon]");typeof o!="undefined"&&(o.href=o.href);let i=dr().pipe(m(u=>u.map(f=>`${new URL(f,n.base)}`)),x(u=>y(document.body,"click").pipe(T(f=>!f.metaKey&&!f.ctrlKey),x(f=>{if(f.target instanceof Element){let p=f.target.closest("a");if(p&&!p.target){let l=new URL(p.href);if(l.search="",l.hash="",l.pathname!==location.pathname&&u.includes(l.toString()))return f.preventDefault(),H({url:new URL(p.href)})}}return xe}))),ae()),a=y(window,"popstate").pipe(T(u=>u.state!==null),m(u=>({url:new URL(location.href),offset:u.state})),ae());C(i,a).pipe(Y((u,f)=>u.url.href===f.url.href),m(({url:u})=>u)).subscribe(t);let s=t.pipe(J("pathname"),x(u=>lr(u.href).pipe(De(()=>(fr(u),xe)))),ae());i.pipe(pt(s)).subscribe(({url:u})=>{history.pushState({},"",`${u}`)});let c=new DOMParser;s.pipe(x(u=>u.text()),m(u=>c.parseFromString(u,"text/html"))).subscribe(e),e.pipe($e(1)).subscribe(u=>{for(let f of["title","link[rel=canonical]","meta[name=author]","meta[name=description]","[data-md-component=announce]","[data-md-component=container]","[data-md-component=header-topic]","[data-md-component=outdated]","[data-md-component=logo]","[data-md-component=skip]",...ce("navigation.tabs.sticky")?["[data-md-component=tabs]"]:[]]){let p=ue(f),l=ue(f,u);typeof p!="undefined"&&typeof l!="undefined"&&p.replaceWith(l)}}),e.pipe($e(1),m(()=>we("container")),x(u=>G("script",u)),$r(u=>{let f=A("script");if(u.src){for(let p of u.getAttributeNames())f.setAttribute(p,u.getAttribute(p));return u.replaceWith(f),new k(p=>{f.onload=()=>p.complete()})}else return f.textContent=u.textContent,u.replaceWith(f),z})).subscribe(),C(i,a).pipe(pt(e)).subscribe(({url:u,offset:f})=>{u.hash&&!f?Po(u.hash):window.scrollTo(0,(f==null?void 0:f.y)||0)}),r.pipe(Ot(i),Xe(250),J("offset")).subscribe(({offset:u})=>{history.replaceState(u,"")}),C(i,a).pipe(Me(2,1),T(([u,f])=>u.url.pathname===f.url.pathname),m(([,u])=>u)).subscribe(({offset:u})=>{window.scrollTo(0,(u==null?void 0:u.y)||0)})}var ws=Ke(Xr());var xi=Ke(Xr());function Zr(e,t){let r=new RegExp(e.separator,"img"),n=(o,i,a)=>`${i}${a}`;return o=>{o=o.replace(/[\s*+\-:~^]+/g," ").trim();let i=new RegExp(`(^|${e.separator})(${o.replace(/[|\\{}()[\]^$+*?.-]/g,"\\$&").replace(r,"|")})`,"img");return a=>(t?(0,xi.default)(a):a).replace(i,n).replace(/<\/mark>(\s+)]*>/img,"$1")}}function Si(e){return e.split(/"([^"]+)"/g).map((t,r)=>r&1?t.replace(/^\b|^(?![^\x00-\x7F]|$)|\s+/g," +"):t).join("").replace(/"|(?:^|\s+)[*+\-:^~]+(?=\s+|$)/g,"").trim()}function ht(e){return e.type===1}function wi(e){return e.type===2}function bt(e){return e.type===3}function Os({config:e,docs:t}){e.lang.length===1&&e.lang[0]==="en"&&(e.lang=[ee("search.config.lang")]),e.separator==="[\\s\\-]+"&&(e.separator=ee("search.config.separator"));let n={pipeline:ee("search.config.pipeline").split(/\s*,\s*/).filter(Boolean),suggestions:ce("search.suggest")};return{config:e,docs:t,options:n}}function Ei(e,t){let r=de(),n=new Worker(e),o=new O,i=zo(n,{tx$:o}).pipe(m(a=>{if(bt(a))for(let s of a.data.items)for(let c of s)c.location=`${new URL(c.location,r.base)}`;return a}),ae());return ne(t).pipe(m(a=>({type:0,data:Os(a)}))).subscribe(o.next.bind(o)),{tx$:o,rx$:i}}function Oi({document$:e}){let t=de(),r=Re(new URL("../versions.json",t.base)),n=r.pipe(m(o=>{let[,i]=t.base.match(/([^/]+)\/?$/);return o.find(({version:a,aliases:s})=>a===i||s.includes(i))||o[0]}));B([r,n]).pipe(m(([o,i])=>new Map(o.filter(a=>a!==i).map(a=>[`${new URL(`../${a.version}/`,t.base)}`,a]))),x(o=>y(document.body,"click").pipe(T(i=>!i.metaKey&&!i.ctrlKey),x(i=>{if(i.target instanceof Element){let a=i.target.closest("a");if(a&&!a.target&&o.has(a.href))return i.preventDefault(),H(a.href)}return z}),x(i=>{let{version:a}=o.get(i);return dr(new URL(i)).pipe(m(s=>{let u=Se().href.replace(t.base,"");return s.includes(u)?new URL(`../${a}/${u}`,t.base):new URL(i)}))})))).subscribe(o=>fr(o)),B([r,n]).subscribe(([o,i])=>{Q(".md-header__topic").appendChild(Go(o,i))}),e.pipe(_t(n)).subscribe(o=>{var a;let i=__md_get("__outdated",sessionStorage);if(i===null){let s=((a=t.version)==null?void 0:a.default)||"latest";i=!o.aliases.includes(s),__md_set("__outdated",i,sessionStorage)}if(i)for(let s of oe("outdated"))s.hidden=!1})}function _s(e,{rx$:t}){let r=(__search==null?void 0:__search.transform)||Si,{searchParams:n}=Se();n.has("q")&&qe("search",!0);let o=t.pipe(T(ht),re(1),m(()=>n.get("q")||""));mt("search").pipe(T(s=>!s),re(1)).subscribe(()=>{let s=new URL(location.href);s.searchParams.delete("q"),history.replaceState({},"",`${s}`)}),o.subscribe(s=>{s&&(e.value=s)});let i=nr(e),a=C(y(e,"keyup"),y(e,"focus").pipe(Ie(1)),o).pipe(m(()=>r(e.value)),q(""),Y());return B([a,i]).pipe(m(([s,c])=>({value:s,focus:c})),X(1))}function _i(e,{tx$:t,rx$:r}){let n=new O;return n.pipe(J("value"),m(({value:o})=>({type:2,data:o}))).subscribe(t.next.bind(t)),n.pipe(J("focus")).subscribe(({focus:o})=>{o?(qe("search",o),e.placeholder=""):e.placeholder=ee("search.placeholder")}),y(e.form,"reset").pipe(se(n.pipe(pe(1)))).subscribe(()=>e.focus()),_s(e,{tx$:t,rx$:r}).pipe(S(o=>n.next(o)),L(()=>n.complete()),m(o=>$({ref:e},o)))}function Ti(e,{rx$:t},{query$:r}){let n=new O,o=Mo(e.parentElement).pipe(T(Boolean)),i=Q(":scope > :first-child",e),a=Q(":scope > :last-child",e),s=t.pipe(T(ht),re(1));return n.pipe(Le(r),Ot(s)).subscribe(([{items:u},{value:f}])=>{if(f)switch(u.length){case 0:i.textContent=ee("search.result.none");break;case 1:i.textContent=ee("search.result.one");break;default:i.textContent=ee("search.result.other",pr(u.length))}else i.textContent=ee("search.result.placeholder")}),n.pipe(S(()=>a.innerHTML=""),x(({items:u})=>C(H(...u.slice(0,10)),H(...u.slice(10)).pipe(Me(4),Nr(o),x(([f])=>f))))).subscribe(u=>a.appendChild(Ko(u))),t.pipe(T(bt),m(({data:u})=>u)).pipe(S(u=>n.next(u)),L(()=>n.complete()),m(u=>$({ref:e},u)))}function Ts(e,{query$:t}){return t.pipe(m(({value:r})=>{let n=Se();return n.hash="",n.searchParams.delete("h"),n.searchParams.set("q",r),{url:n}}))}function Mi(e,t){let r=new O;return r.subscribe(({url:n})=>{e.setAttribute("data-clipboard-text",e.href),e.href=`${n}`}),y(e,"click").subscribe(n=>n.preventDefault()),Ts(e,t).pipe(S(n=>r.next(n)),L(()=>r.complete()),m(n=>$({ref:e},n)))}function Li(e,{rx$:t},{keyboard$:r}){let n=new O,o=we("search-query"),i=C(y(o,"keydown"),y(o,"focus")).pipe(Be(le),m(()=>o.value),Y());return n.pipe(Ve(i),m(([{suggestions:s},c])=>{let u=c.split(/([\s-]+)/);if((s==null?void 0:s.length)&&u[u.length-1]){let f=s[s.length-1];f.startsWith(u[u.length-1])&&(u[u.length-1]=f)}else u.length=0;return u})).subscribe(s=>e.innerHTML=s.join("").replace(/\s/g," ")),r.pipe(T(({mode:s})=>s==="search")).subscribe(s=>{switch(s.type){case"ArrowRight":e.innerText.length&&o.selectionStart===o.value.length&&(o.value=e.innerText);break}}),t.pipe(T(bt),m(({data:s})=>s)).pipe(S(s=>n.next(s)),L(()=>n.complete()),m(()=>({ref:e})))}function Ai(e,{index$:t,keyboard$:r}){let n=de();try{let o=(__search==null?void 0:__search.worker)||n.search,i=Ei(o,t),a=we("search-query",e),s=we("search-result",e),{tx$:c,rx$:u}=i;c.pipe(T(wi),pt(u.pipe(T(ht))),re(1)).subscribe(c.next.bind(c)),r.pipe(T(({mode:l})=>l==="search")).subscribe(l=>{let d=Ne();switch(l.type){case"Enter":if(d===a){let h=new Map;for(let b of G(":first-child [href]",s)){let F=b.firstElementChild;h.set(b,parseFloat(F.getAttribute("data-md-score")))}if(h.size){let[[b]]=[...h].sort(([,F],[,K])=>K-F);b.click()}l.claim()}break;case"Escape":case"Tab":qe("search",!1),a.blur();break;case"ArrowUp":case"ArrowDown":if(typeof d=="undefined")a.focus();else{let h=[a,...G(":not(details) > [href], summary, details[open] [href]",s)],b=Math.max(0,(Math.max(0,h.indexOf(d))+h.length+(l.type==="ArrowUp"?-1:1))%h.length);h[b].focus()}l.claim();break;default:a!==Ne()&&a.focus()}}),r.pipe(T(({mode:l})=>l==="global")).subscribe(l=>{switch(l.type){case"f":case"s":case"/":a.focus(),a.select(),l.claim();break}});let f=_i(a,i),p=Ti(s,i,{query$:f});return C(f,p).pipe(Ze(...oe("search-share",e).map(l=>Mi(l,{query$:f})),...oe("search-suggest",e).map(l=>Li(l,i,{keyboard$:r}))))}catch(o){return e.hidden=!0,xe}}function Ci(e,{index$:t,location$:r}){return B([t,r.pipe(q(Se()),T(n=>!!n.searchParams.get("h")))]).pipe(m(([n,o])=>Zr(n.config,!0)(o.searchParams.get("h"))),m(n=>{var a;let o=new Map,i=document.createNodeIterator(e,NodeFilter.SHOW_TEXT);for(let s=i.nextNode();s;s=i.nextNode())if((a=s.parentElement)!=null&&a.offsetHeight){let c=s.textContent,u=n(c);u.length>c.length&&o.set(s,u)}for(let[s,c]of o){let{childNodes:u}=A("span",null,c);s.replaceWith(...Array.from(u))}return{ref:e,nodes:o}}))}function Ms(e,{viewport$:t,main$:r}){let n=e.parentElement,o=n.offsetTop-n.parentElement.offsetTop;return B([r,t]).pipe(m(([{offset:i,height:a},{offset:{y:s}}])=>(a=a+Math.min(o,Math.max(0,s-i))-o,{height:a,locked:s>=i+o})),Y((i,a)=>i.height===a.height&&i.locked===a.locked))}function en(e,n){var o=n,{header$:t}=o,r=sn(o,["header$"]);let i=Q(".md-sidebar__scrollwrap",e),{y:a}=ze(i);return P(()=>{let s=new O;return s.pipe(He(0,Te),Le(t)).subscribe({next([{height:c},{height:u}]){i.style.height=`${c-2*a}px`,e.style.top=`${u}px`},complete(){i.style.height="",e.style.top=""}}),Ms(e,r).pipe(S(c=>s.next(c)),L(()=>s.complete()),m(c=>$({ref:e},c)))})}function Ri(e,t){if(typeof t!="undefined"){let r=`https://api.github.com/repos/${e}/${t}`;return wt(Re(`${r}/releases/latest`).pipe(m(n=>({version:n.tag_name})),Pe({})),Re(r).pipe(m(n=>({stars:n.stargazers_count,forks:n.forks_count})),Pe({}))).pipe(m(([n,o])=>$($({},n),o)))}else{let r=`https://api.github.com/users/${e}`;return Re(r).pipe(m(n=>({repositories:n.public_repos})),Pe({}))}}function ki(e,t){let r=`https://${e}/api/v4/projects/${encodeURIComponent(t)}`;return Re(r).pipe(m(({star_count:n,forks_count:o})=>({stars:n,forks:o})),Pe({}))}function Hi(e){let[t]=e.match(/(git(?:hub|lab))/i)||[];switch(t.toLowerCase()){case"github":let[,r,n]=e.match(/^.+github\.com\/([^/]+)\/?([^/]+)?/i);return Ri(r,n);case"gitlab":let[,o,i]=e.match(/^.+?([^/]*gitlab[^/]+)\/(.+?)\/?$/i);return ki(o,i);default:return z}}var Ls;function As(e){return Ls||(Ls=P(()=>{let t=__md_get("__source",sessionStorage);return t?H(t):Hi(e.href).pipe(S(r=>__md_set("__source",r,sessionStorage)))}).pipe(De(()=>z),T(t=>Object.keys(t).length>0),m(t=>({facts:t})),X(1)))}function Pi(e){let t=Q(":scope > :last-child",e);return P(()=>{let r=new O;return r.subscribe(({facts:n})=>{t.appendChild(Yo(n)),t.setAttribute("data-md-state","done")}),As(e).pipe(S(n=>r.next(n)),L(()=>r.complete()),m(n=>$({ref:e},n)))})}function Cs(e,{viewport$:t,header$:r}){return ge(document.body).pipe(x(()=>mr(e,{header$:r,viewport$:t})),m(({offset:{y:n}})=>({hidden:n>=10})),J("hidden"))}function Ii(e,t){return P(()=>{let r=new O;return r.subscribe({next({hidden:n}){n?e.setAttribute("data-md-state","hidden"):e.removeAttribute("data-md-state")},complete(){e.removeAttribute("data-md-state")}}),(ce("navigation.tabs.sticky")?H({hidden:!1}):Cs(e,t)).pipe(S(n=>r.next(n)),L(()=>r.complete()),m(n=>$({ref:e},n)))})}function Rs(e,{viewport$:t,header$:r}){let n=new Map,o=G("[href^=\\#]",e);for(let s of o){let c=decodeURIComponent(s.hash.substring(1)),u=ue(`[id="${c}"]`);typeof u!="undefined"&&n.set(s,u)}let i=r.pipe(J("height"),m(({height:s})=>{let c=we("main"),u=Q(":scope > :first-child",c);return s+.8*(u.offsetTop-c.offsetTop)}),ae());return ge(document.body).pipe(J("height"),x(s=>P(()=>{let c=[];return H([...n].reduce((u,[f,p])=>{for(;c.length&&n.get(c[c.length-1]).tagName>=p.tagName;)c.pop();let l=p.offsetTop;for(;!l&&p.parentElement;)p=p.parentElement,l=p.offsetTop;return u.set([...c=[...c,f]].reverse(),l)},new Map))}).pipe(m(c=>new Map([...c].sort(([,u],[,f])=>u-f))),Ve(i),x(([c,u])=>t.pipe(Fr(([f,p],{offset:{y:l},size:d})=>{let h=l+d.height>=Math.floor(s.height);for(;p.length;){let[,b]=p[0];if(b-u=l&&!h)p=[f.pop(),...p];else break}return[f,p]},[[],[...c]]),Y((f,p)=>f[0]===p[0]&&f[1]===p[1])))))).pipe(m(([s,c])=>({prev:s.map(([u])=>u),next:c.map(([u])=>u)})),q({prev:[],next:[]}),Me(2,1),m(([s,c])=>s.prev.length{let o=new O;return o.subscribe(({prev:i,next:a})=>{for(let[s]of a)s.removeAttribute("data-md-state"),s.classList.remove("md-nav__link--active");for(let[s,[c]]of i.entries())c.setAttribute("data-md-state","blur"),c.classList.toggle("md-nav__link--active",s===i.length-1)}),ce("navigation.tracking")&&t.pipe(se(o.pipe(pe(1))),J("offset"),Xe(250),$e(1),se(n.pipe($e(1))),Et({delay:250}),Le(o)).subscribe(([,{prev:i}])=>{let a=Se(),s=i[i.length-1];if(s&&s.length){let[c]=s,{hash:u}=new URL(c.href);a.hash!==u&&(a.hash=u,history.replaceState({},"",`${a}`))}else a.hash="",history.replaceState({},"",`${a}`)}),Rs(e,{viewport$:t,header$:r}).pipe(S(i=>o.next(i)),L(()=>o.complete()),m(i=>$({ref:e},i)))})}function ks(e,{viewport$:t,main$:r,target$:n}){let o=t.pipe(m(({offset:{y:a}})=>a),Me(2,1),m(([a,s])=>a>s&&s>0),Y()),i=r.pipe(m(({active:a})=>a));return B([i,o]).pipe(m(([a,s])=>!(a&&s)),Y(),se(n.pipe($e(1))),rr(!0),Et({delay:250}),m(a=>({hidden:a})))}function ji(e,{viewport$:t,header$:r,main$:n,target$:o}){let i=new O;return i.subscribe({next({hidden:a}){a?(e.setAttribute("data-md-state","hidden"),e.setAttribute("tabindex","-1"),e.blur()):(e.removeAttribute("data-md-state"),e.removeAttribute("tabindex"))},complete(){e.style.top="",e.setAttribute("data-md-state","hidden"),e.removeAttribute("tabindex")}}),r.pipe(se(i.pipe(rr(0),pe(1))),J("height")).subscribe(({height:a})=>{e.style.top=`${a+16}px`}),ks(e,{viewport$:t,main$:n,target$:o}).pipe(S(a=>i.next(a)),L(()=>i.complete()),m(a=>$({ref:e},a)))}function Fi({document$:e,tablet$:t}){e.pipe(x(()=>G("[data-md-state=indeterminate]")),S(r=>{r.indeterminate=!0,r.checked=!1}),ie(r=>y(r,"change").pipe(Wr(()=>r.hasAttribute("data-md-state")),Z(r))),Le(t)).subscribe(([r,n])=>{r.removeAttribute("data-md-state"),n&&(r.checked=!1)})}function Hs(){return/(iPad|iPhone|iPod)/.test(navigator.userAgent)}function Ui({document$:e}){e.pipe(x(()=>G("[data-md-scrollfix]")),S(t=>t.removeAttribute("data-md-scrollfix")),T(Hs),ie(t=>y(t,"touchstart").pipe(Z(t)))).subscribe(t=>{let r=t.scrollTop;r===0?t.scrollTop=1:r+t.offsetHeight===t.scrollHeight&&(t.scrollTop=r-1)})}function Wi({viewport$:e,tablet$:t}){B([mt("search"),t]).pipe(m(([r,n])=>r&&!n),x(r=>H(r).pipe(Ie(r?400:100))),Le(e)).subscribe(([r,{offset:{y:n}}])=>{if(r)document.body.setAttribute("data-md-state","lock"),document.body.style.top=`-${n}px`;else{let o=-1*parseInt(document.body.style.top,10);document.body.removeAttribute("data-md-state"),document.body.style.top="",o&&window.scrollTo(0,o)}})}Object.entries||(Object.entries=function(e){let t=[];for(let r of Object.keys(e))t.push([r,e[r]]);return t});Object.values||(Object.values=function(e){let t=[];for(let r of Object.keys(e))t.push(e[r]);return t});typeof Element!="undefined"&&(Element.prototype.scrollTo||(Element.prototype.scrollTo=function(e,t){typeof e=="object"?(this.scrollLeft=e.left,this.scrollTop=e.top):(this.scrollLeft=e,this.scrollTop=t)}),Element.prototype.replaceWith||(Element.prototype.replaceWith=function(...e){let t=this.parentNode;if(t){e.length===0&&t.removeChild(this);for(let r=e.length-1;r>=0;r--){let n=e[r];typeof n!="object"?n=document.createTextNode(n):n.parentNode&&n.parentNode.removeChild(n),r?t.insertBefore(this.previousSibling,n):t.replaceChild(n,this)}}}));document.documentElement.classList.remove("no-js");document.documentElement.classList.add("js");var et=mo(),br=Co(),Lt=Io(),tn=Ao(),Ee=No(),vr=qr("(min-width: 960px)"),Vi=qr("(min-width: 1220px)"),Ni=$o(),zi=de(),qi=document.forms.namedItem("search")?(__search==null?void 0:__search.index)||Re(new URL("search/search_index.json",zi.base)):xe,rn=new O;vi({alert$:rn});ce("navigation.instant")&&gi({document$:et,location$:br,viewport$:Ee});var Di;((Di=zi.version)==null?void 0:Di.provider)==="mike"&&Oi({document$:et});C(br,Lt).pipe(Ie(125)).subscribe(()=>{qe("drawer",!1),qe("search",!1)});tn.pipe(T(({mode:e})=>e==="global")).subscribe(e=>{switch(e.type){case"p":case",":let t=ue("[href][rel=prev]");typeof t!="undefined"&&t.click();break;case"n":case".":let r=ue("[href][rel=next]");typeof r!="undefined"&&r.click();break}});Fi({document$:et,tablet$:vr});Ui({document$:et});Wi({viewport$:Ee,tablet$:vr});var Qe=li(we("header"),{viewport$:Ee}),hr=et.pipe(m(()=>we("main")),x(e=>hi(e,{viewport$:Ee,header$:Qe})),X(1)),Ps=C(...oe("dialog").map(e=>pi(e,{alert$:rn})),...oe("header").map(e=>mi(e,{viewport$:Ee,header$:Qe,main$:hr})),...oe("palette").map(e=>bi(e)),...oe("search").map(e=>Ai(e,{index$:qi,keyboard$:tn})),...oe("source").map(e=>Pi(e))),Is=P(()=>C(...oe("content").map(e=>fi(e,{target$:Lt,print$:Ni})),...oe("content").map(e=>ce("search.highlight")?Ci(e,{index$:qi,location$:br}):z),...oe("header-title").map(e=>di(e,{viewport$:Ee,header$:Qe})),...oe("sidebar").map(e=>e.getAttribute("data-md-type")==="navigation"?Qr(Vi,()=>en(e,{viewport$:Ee,header$:Qe,main$:hr})):Qr(vr,()=>en(e,{viewport$:Ee,header$:Qe,main$:hr}))),...oe("tabs").map(e=>Ii(e,{viewport$:Ee,header$:Qe})),...oe("toc").map(e=>$i(e,{viewport$:Ee,header$:Qe,target$:Lt})),...oe("top").map(e=>ji(e,{viewport$:Ee,header$:Qe,main$:hr,target$:Lt})))),Qi=et.pipe(x(()=>Is),Ze(Ps),X(1));Qi.subscribe();window.document$=et;window.location$=br;window.target$=Lt;window.keyboard$=tn;window.viewport$=Ee;window.tablet$=vr;window.screen$=Vi;window.print$=Ni;window.alert$=rn;window.component$=Qi;})(); +//# sourceMappingURL=bundle.c44cc438.min.js.map + diff --git a/v0.13.6/assets/javascripts/bundle.c44cc438.min.js.map b/v0.13.6/assets/javascripts/bundle.c44cc438.min.js.map new file mode 100644 index 0000000000..13182229a4 --- /dev/null +++ b/v0.13.6/assets/javascripts/bundle.c44cc438.min.js.map @@ -0,0 +1,8 @@ +{ + "version": 3, + "sources": ["node_modules/focus-visible/dist/focus-visible.js", "node_modules/url-polyfill/url-polyfill.js", "node_modules/rxjs/node_modules/tslib/tslib.js", "node_modules/clipboard/dist/clipboard.js", "node_modules/escape-html/index.js", "node_modules/array-flat-polyfill/index.mjs", "src/assets/javascripts/bundle.ts", "node_modules/unfetch/polyfill/index.js", "node_modules/rxjs/node_modules/tslib/modules/index.js", "node_modules/rxjs/src/internal/util/isFunction.ts", "node_modules/rxjs/src/internal/util/createErrorClass.ts", "node_modules/rxjs/src/internal/util/UnsubscriptionError.ts", "node_modules/rxjs/src/internal/util/arrRemove.ts", "node_modules/rxjs/src/internal/Subscription.ts", "node_modules/rxjs/src/internal/config.ts", "node_modules/rxjs/src/internal/scheduler/timeoutProvider.ts", "node_modules/rxjs/src/internal/util/reportUnhandledError.ts", "node_modules/rxjs/src/internal/util/noop.ts", "node_modules/rxjs/src/internal/NotificationFactories.ts", "node_modules/rxjs/src/internal/util/errorContext.ts", "node_modules/rxjs/src/internal/Subscriber.ts", "node_modules/rxjs/src/internal/symbol/observable.ts", "node_modules/rxjs/src/internal/util/identity.ts", "node_modules/rxjs/src/internal/util/pipe.ts", "node_modules/rxjs/src/internal/Observable.ts", "node_modules/rxjs/src/internal/util/lift.ts", "node_modules/rxjs/src/internal/operators/OperatorSubscriber.ts", "node_modules/rxjs/src/internal/scheduler/animationFrameProvider.ts", "node_modules/rxjs/src/internal/util/ObjectUnsubscribedError.ts", "node_modules/rxjs/src/internal/Subject.ts", "node_modules/rxjs/src/internal/scheduler/dateTimestampProvider.ts", "node_modules/rxjs/src/internal/ReplaySubject.ts", "node_modules/rxjs/src/internal/scheduler/Action.ts", "node_modules/rxjs/src/internal/scheduler/intervalProvider.ts", "node_modules/rxjs/src/internal/scheduler/AsyncAction.ts", "node_modules/rxjs/src/internal/Scheduler.ts", "node_modules/rxjs/src/internal/scheduler/AsyncScheduler.ts", "node_modules/rxjs/src/internal/scheduler/async.ts", "node_modules/rxjs/src/internal/scheduler/AnimationFrameAction.ts", "node_modules/rxjs/src/internal/scheduler/AnimationFrameScheduler.ts", "node_modules/rxjs/src/internal/scheduler/animationFrame.ts", "node_modules/rxjs/src/internal/observable/empty.ts", "node_modules/rxjs/src/internal/util/isScheduler.ts", "node_modules/rxjs/src/internal/util/args.ts", "node_modules/rxjs/src/internal/util/isArrayLike.ts", "node_modules/rxjs/src/internal/util/isPromise.ts", "node_modules/rxjs/src/internal/util/isInteropObservable.ts", "node_modules/rxjs/src/internal/util/isAsyncIterable.ts", "node_modules/rxjs/src/internal/util/throwUnobservableError.ts", "node_modules/rxjs/src/internal/symbol/iterator.ts", "node_modules/rxjs/src/internal/util/isIterable.ts", "node_modules/rxjs/src/internal/util/isReadableStreamLike.ts", "node_modules/rxjs/src/internal/observable/innerFrom.ts", "node_modules/rxjs/src/internal/util/executeSchedule.ts", "node_modules/rxjs/src/internal/operators/observeOn.ts", "node_modules/rxjs/src/internal/operators/subscribeOn.ts", "node_modules/rxjs/src/internal/scheduled/scheduleObservable.ts", "node_modules/rxjs/src/internal/scheduled/schedulePromise.ts", "node_modules/rxjs/src/internal/scheduled/scheduleArray.ts", "node_modules/rxjs/src/internal/scheduled/scheduleIterable.ts", "node_modules/rxjs/src/internal/scheduled/scheduleAsyncIterable.ts", "node_modules/rxjs/src/internal/scheduled/scheduleReadableStreamLike.ts", "node_modules/rxjs/src/internal/scheduled/scheduled.ts", "node_modules/rxjs/src/internal/observable/from.ts", "node_modules/rxjs/src/internal/observable/of.ts", "node_modules/rxjs/src/internal/observable/throwError.ts", "node_modules/rxjs/src/internal/util/isDate.ts", "node_modules/rxjs/src/internal/operators/map.ts", "node_modules/rxjs/src/internal/util/mapOneOrManyArgs.ts", "node_modules/rxjs/src/internal/util/argsArgArrayOrObject.ts", "node_modules/rxjs/src/internal/util/createObject.ts", "node_modules/rxjs/src/internal/observable/combineLatest.ts", "node_modules/rxjs/src/internal/operators/mergeInternals.ts", "node_modules/rxjs/src/internal/operators/mergeMap.ts", "node_modules/rxjs/src/internal/operators/mergeAll.ts", "node_modules/rxjs/src/internal/operators/concatAll.ts", "node_modules/rxjs/src/internal/observable/concat.ts", "node_modules/rxjs/src/internal/observable/defer.ts", "node_modules/rxjs/src/internal/observable/fromEvent.ts", "node_modules/rxjs/src/internal/observable/fromEventPattern.ts", "node_modules/rxjs/src/internal/observable/timer.ts", "node_modules/rxjs/src/internal/observable/merge.ts", "node_modules/rxjs/src/internal/observable/never.ts", "node_modules/rxjs/src/internal/util/argsOrArgArray.ts", "node_modules/rxjs/src/internal/operators/filter.ts", "node_modules/rxjs/src/internal/observable/zip.ts", "node_modules/rxjs/src/internal/operators/audit.ts", "node_modules/rxjs/src/internal/operators/auditTime.ts", "node_modules/rxjs/src/internal/operators/bufferCount.ts", "node_modules/rxjs/src/internal/operators/catchError.ts", "node_modules/rxjs/src/internal/operators/scanInternals.ts", "node_modules/rxjs/src/internal/operators/combineLatest.ts", "node_modules/rxjs/src/internal/operators/combineLatestWith.ts", "node_modules/rxjs/src/internal/operators/concatMap.ts", "node_modules/rxjs/src/internal/operators/debounceTime.ts", "node_modules/rxjs/src/internal/operators/defaultIfEmpty.ts", "node_modules/rxjs/src/internal/operators/take.ts", "node_modules/rxjs/src/internal/operators/ignoreElements.ts", "node_modules/rxjs/src/internal/operators/mapTo.ts", "node_modules/rxjs/src/internal/operators/delayWhen.ts", "node_modules/rxjs/src/internal/operators/delay.ts", "node_modules/rxjs/src/internal/operators/distinctUntilChanged.ts", "node_modules/rxjs/src/internal/operators/distinctUntilKeyChanged.ts", "node_modules/rxjs/src/internal/operators/endWith.ts", "node_modules/rxjs/src/internal/operators/finalize.ts", "node_modules/rxjs/src/internal/operators/takeLast.ts", "node_modules/rxjs/src/internal/operators/merge.ts", "node_modules/rxjs/src/internal/operators/mergeWith.ts", "node_modules/rxjs/src/internal/operators/repeat.ts", "node_modules/rxjs/src/internal/operators/sample.ts", "node_modules/rxjs/src/internal/operators/scan.ts", "node_modules/rxjs/src/internal/operators/share.ts", "node_modules/rxjs/src/internal/operators/shareReplay.ts", "node_modules/rxjs/src/internal/operators/skip.ts", "node_modules/rxjs/src/internal/operators/skipUntil.ts", "node_modules/rxjs/src/internal/operators/startWith.ts", "node_modules/rxjs/src/internal/operators/switchMap.ts", "node_modules/rxjs/src/internal/operators/switchMapTo.ts", "node_modules/rxjs/src/internal/operators/takeUntil.ts", "node_modules/rxjs/src/internal/operators/takeWhile.ts", "node_modules/rxjs/src/internal/operators/tap.ts", "node_modules/rxjs/src/internal/operators/throttle.ts", "node_modules/rxjs/src/internal/operators/throttleTime.ts", "node_modules/rxjs/src/internal/operators/withLatestFrom.ts", "node_modules/rxjs/src/internal/operators/zip.ts", "node_modules/rxjs/src/internal/operators/zipWith.ts", "src/assets/javascripts/browser/document/index.ts", "src/assets/javascripts/browser/element/_/index.ts", "src/assets/javascripts/browser/element/focus/index.ts", "src/assets/javascripts/browser/element/offset/_/index.ts", "src/assets/javascripts/browser/element/offset/content/index.ts", "node_modules/resize-observer-polyfill/dist/ResizeObserver.es.js", "src/assets/javascripts/browser/element/size/_/index.ts", "src/assets/javascripts/browser/element/size/content/index.ts", "src/assets/javascripts/browser/element/visibility/index.ts", "src/assets/javascripts/browser/toggle/index.ts", "src/assets/javascripts/browser/keyboard/index.ts", "src/assets/javascripts/browser/location/_/index.ts", "src/assets/javascripts/utilities/h/index.ts", "src/assets/javascripts/utilities/string/index.ts", "src/assets/javascripts/browser/location/hash/index.ts", "src/assets/javascripts/browser/media/index.ts", "src/assets/javascripts/browser/request/index.ts", "src/assets/javascripts/browser/script/index.ts", "src/assets/javascripts/browser/viewport/offset/index.ts", "src/assets/javascripts/browser/viewport/size/index.ts", "src/assets/javascripts/browser/viewport/_/index.ts", "src/assets/javascripts/browser/viewport/at/index.ts", "src/assets/javascripts/browser/worker/index.ts", "src/assets/javascripts/_/index.ts", "src/assets/javascripts/components/_/index.ts", "src/assets/javascripts/components/content/code/_/index.ts", "src/assets/javascripts/templates/annotation/index.tsx", "src/assets/javascripts/templates/clipboard/index.tsx", "src/assets/javascripts/templates/search/index.tsx", "src/assets/javascripts/templates/source/index.tsx", "src/assets/javascripts/templates/table/index.tsx", "src/assets/javascripts/templates/version/index.tsx", "src/assets/javascripts/components/content/annotation/_/index.ts", "src/assets/javascripts/components/content/annotation/list/index.ts", "src/assets/javascripts/components/content/code/mermaid/index.ts", "src/assets/javascripts/components/content/details/index.ts", "src/assets/javascripts/components/content/table/index.ts", "src/assets/javascripts/components/content/tabs/index.ts", "src/assets/javascripts/components/content/_/index.ts", "src/assets/javascripts/components/dialog/index.ts", "src/assets/javascripts/components/header/_/index.ts", "src/assets/javascripts/components/header/title/index.ts", "src/assets/javascripts/components/main/index.ts", "src/assets/javascripts/components/palette/index.ts", "src/assets/javascripts/integrations/clipboard/index.ts", "src/assets/javascripts/integrations/sitemap/index.ts", "src/assets/javascripts/integrations/instant/index.ts", "src/assets/javascripts/integrations/search/document/index.ts", "src/assets/javascripts/integrations/search/highlighter/index.ts", "src/assets/javascripts/integrations/search/query/transform/index.ts", "src/assets/javascripts/integrations/search/worker/message/index.ts", "src/assets/javascripts/integrations/search/worker/_/index.ts", "src/assets/javascripts/integrations/version/index.ts", "src/assets/javascripts/components/search/query/index.ts", "src/assets/javascripts/components/search/result/index.ts", "src/assets/javascripts/components/search/share/index.ts", "src/assets/javascripts/components/search/suggest/index.ts", "src/assets/javascripts/components/search/_/index.ts", "src/assets/javascripts/components/search/highlight/index.ts", "src/assets/javascripts/components/sidebar/index.ts", "src/assets/javascripts/components/source/facts/github/index.ts", "src/assets/javascripts/components/source/facts/gitlab/index.ts", "src/assets/javascripts/components/source/facts/_/index.ts", "src/assets/javascripts/components/source/_/index.ts", "src/assets/javascripts/components/tabs/index.ts", "src/assets/javascripts/components/toc/index.ts", "src/assets/javascripts/components/top/index.ts", "src/assets/javascripts/patches/indeterminate/index.ts", "src/assets/javascripts/patches/scrollfix/index.ts", "src/assets/javascripts/patches/scrolllock/index.ts", "src/assets/javascripts/polyfills/index.ts"], + "sourceRoot": "../../../..", + "sourcesContent": ["(function (global, factory) {\n typeof exports === 'object' && typeof module !== 'undefined' ? factory() :\n typeof define === 'function' && define.amd ? define(factory) :\n (factory());\n}(this, (function () { 'use strict';\n\n /**\n * Applies the :focus-visible polyfill at the given scope.\n * A scope in this case is either the top-level Document or a Shadow Root.\n *\n * @param {(Document|ShadowRoot)} scope\n * @see https://github.com/WICG/focus-visible\n */\n function applyFocusVisiblePolyfill(scope) {\n var hadKeyboardEvent = true;\n var hadFocusVisibleRecently = false;\n var hadFocusVisibleRecentlyTimeout = null;\n\n var inputTypesAllowlist = {\n text: true,\n search: true,\n url: true,\n tel: true,\n email: true,\n password: true,\n number: true,\n date: true,\n month: true,\n week: true,\n time: true,\n datetime: true,\n 'datetime-local': true\n };\n\n /**\n * Helper function for legacy browsers and iframes which sometimes focus\n * elements like document, body, and non-interactive SVG.\n * @param {Element} el\n */\n function isValidFocusTarget(el) {\n if (\n el &&\n el !== document &&\n el.nodeName !== 'HTML' &&\n el.nodeName !== 'BODY' &&\n 'classList' in el &&\n 'contains' in el.classList\n ) {\n return true;\n }\n return false;\n }\n\n /**\n * Computes whether the given element should automatically trigger the\n * `focus-visible` class being added, i.e. whether it should always match\n * `:focus-visible` when focused.\n * @param {Element} el\n * @return {boolean}\n */\n function focusTriggersKeyboardModality(el) {\n var type = el.type;\n var tagName = el.tagName;\n\n if (tagName === 'INPUT' && inputTypesAllowlist[type] && !el.readOnly) {\n return true;\n }\n\n if (tagName === 'TEXTAREA' && !el.readOnly) {\n return true;\n }\n\n if (el.isContentEditable) {\n return true;\n }\n\n return false;\n }\n\n /**\n * Add the `focus-visible` class to the given element if it was not added by\n * the author.\n * @param {Element} el\n */\n function addFocusVisibleClass(el) {\n if (el.classList.contains('focus-visible')) {\n return;\n }\n el.classList.add('focus-visible');\n el.setAttribute('data-focus-visible-added', '');\n }\n\n /**\n * Remove the `focus-visible` class from the given element if it was not\n * originally added by the author.\n * @param {Element} el\n */\n function removeFocusVisibleClass(el) {\n if (!el.hasAttribute('data-focus-visible-added')) {\n return;\n }\n el.classList.remove('focus-visible');\n el.removeAttribute('data-focus-visible-added');\n }\n\n /**\n * If the most recent user interaction was via the keyboard;\n * and the key press did not include a meta, alt/option, or control key;\n * then the modality is keyboard. Otherwise, the modality is not keyboard.\n * Apply `focus-visible` to any current active element and keep track\n * of our keyboard modality state with `hadKeyboardEvent`.\n * @param {KeyboardEvent} e\n */\n function onKeyDown(e) {\n if (e.metaKey || e.altKey || e.ctrlKey) {\n return;\n }\n\n if (isValidFocusTarget(scope.activeElement)) {\n addFocusVisibleClass(scope.activeElement);\n }\n\n hadKeyboardEvent = true;\n }\n\n /**\n * If at any point a user clicks with a pointing device, ensure that we change\n * the modality away from keyboard.\n * This avoids the situation where a user presses a key on an already focused\n * element, and then clicks on a different element, focusing it with a\n * pointing device, while we still think we're in keyboard modality.\n * @param {Event} e\n */\n function onPointerDown(e) {\n hadKeyboardEvent = false;\n }\n\n /**\n * On `focus`, add the `focus-visible` class to the target if:\n * - the target received focus as a result of keyboard navigation, or\n * - the event target is an element that will likely require interaction\n * via the keyboard (e.g. a text box)\n * @param {Event} e\n */\n function onFocus(e) {\n // Prevent IE from focusing the document or HTML element.\n if (!isValidFocusTarget(e.target)) {\n return;\n }\n\n if (hadKeyboardEvent || focusTriggersKeyboardModality(e.target)) {\n addFocusVisibleClass(e.target);\n }\n }\n\n /**\n * On `blur`, remove the `focus-visible` class from the target.\n * @param {Event} e\n */\n function onBlur(e) {\n if (!isValidFocusTarget(e.target)) {\n return;\n }\n\n if (\n e.target.classList.contains('focus-visible') ||\n e.target.hasAttribute('data-focus-visible-added')\n ) {\n // To detect a tab/window switch, we look for a blur event followed\n // rapidly by a visibility change.\n // If we don't see a visibility change within 100ms, it's probably a\n // regular focus change.\n hadFocusVisibleRecently = true;\n window.clearTimeout(hadFocusVisibleRecentlyTimeout);\n hadFocusVisibleRecentlyTimeout = window.setTimeout(function() {\n hadFocusVisibleRecently = false;\n }, 100);\n removeFocusVisibleClass(e.target);\n }\n }\n\n /**\n * If the user changes tabs, keep track of whether or not the previously\n * focused element had .focus-visible.\n * @param {Event} e\n */\n function onVisibilityChange(e) {\n if (document.visibilityState === 'hidden') {\n // If the tab becomes active again, the browser will handle calling focus\n // on the element (Safari actually calls it twice).\n // If this tab change caused a blur on an element with focus-visible,\n // re-apply the class when the user switches back to the tab.\n if (hadFocusVisibleRecently) {\n hadKeyboardEvent = true;\n }\n addInitialPointerMoveListeners();\n }\n }\n\n /**\n * Add a group of listeners to detect usage of any pointing devices.\n * These listeners will be added when the polyfill first loads, and anytime\n * the window is blurred, so that they are active when the window regains\n * focus.\n */\n function addInitialPointerMoveListeners() {\n document.addEventListener('mousemove', onInitialPointerMove);\n document.addEventListener('mousedown', onInitialPointerMove);\n document.addEventListener('mouseup', onInitialPointerMove);\n document.addEventListener('pointermove', onInitialPointerMove);\n document.addEventListener('pointerdown', onInitialPointerMove);\n document.addEventListener('pointerup', onInitialPointerMove);\n document.addEventListener('touchmove', onInitialPointerMove);\n document.addEventListener('touchstart', onInitialPointerMove);\n document.addEventListener('touchend', onInitialPointerMove);\n }\n\n function removeInitialPointerMoveListeners() {\n document.removeEventListener('mousemove', onInitialPointerMove);\n document.removeEventListener('mousedown', onInitialPointerMove);\n document.removeEventListener('mouseup', onInitialPointerMove);\n document.removeEventListener('pointermove', onInitialPointerMove);\n document.removeEventListener('pointerdown', onInitialPointerMove);\n document.removeEventListener('pointerup', onInitialPointerMove);\n document.removeEventListener('touchmove', onInitialPointerMove);\n document.removeEventListener('touchstart', onInitialPointerMove);\n document.removeEventListener('touchend', onInitialPointerMove);\n }\n\n /**\n * When the polfyill first loads, assume the user is in keyboard modality.\n * If any event is received from a pointing device (e.g. mouse, pointer,\n * touch), turn off keyboard modality.\n * This accounts for situations where focus enters the page from the URL bar.\n * @param {Event} e\n */\n function onInitialPointerMove(e) {\n // Work around a Safari quirk that fires a mousemove on whenever the\n // window blurs, even if you're tabbing out of the page. \u00AF\\_(\u30C4)_/\u00AF\n if (e.target.nodeName && e.target.nodeName.toLowerCase() === 'html') {\n return;\n }\n\n hadKeyboardEvent = false;\n removeInitialPointerMoveListeners();\n }\n\n // For some kinds of state, we are interested in changes at the global scope\n // only. For example, global pointer input, global key presses and global\n // visibility change should affect the state at every scope:\n document.addEventListener('keydown', onKeyDown, true);\n document.addEventListener('mousedown', onPointerDown, true);\n document.addEventListener('pointerdown', onPointerDown, true);\n document.addEventListener('touchstart', onPointerDown, true);\n document.addEventListener('visibilitychange', onVisibilityChange, true);\n\n addInitialPointerMoveListeners();\n\n // For focus and blur, we specifically care about state changes in the local\n // scope. This is because focus / blur events that originate from within a\n // shadow root are not re-dispatched from the host element if it was already\n // the active element in its own scope:\n scope.addEventListener('focus', onFocus, true);\n scope.addEventListener('blur', onBlur, true);\n\n // We detect that a node is a ShadowRoot by ensuring that it is a\n // DocumentFragment and also has a host property. This check covers native\n // implementation and polyfill implementation transparently. If we only cared\n // about the native implementation, we could just check if the scope was\n // an instance of a ShadowRoot.\n if (scope.nodeType === Node.DOCUMENT_FRAGMENT_NODE && scope.host) {\n // Since a ShadowRoot is a special kind of DocumentFragment, it does not\n // have a root element to add a class to. So, we add this attribute to the\n // host element instead:\n scope.host.setAttribute('data-js-focus-visible', '');\n } else if (scope.nodeType === Node.DOCUMENT_NODE) {\n document.documentElement.classList.add('js-focus-visible');\n document.documentElement.setAttribute('data-js-focus-visible', '');\n }\n }\n\n // It is important to wrap all references to global window and document in\n // these checks to support server-side rendering use cases\n // @see https://github.com/WICG/focus-visible/issues/199\n if (typeof window !== 'undefined' && typeof document !== 'undefined') {\n // Make the polyfill helper globally available. This can be used as a signal\n // to interested libraries that wish to coordinate with the polyfill for e.g.,\n // applying the polyfill to a shadow root:\n window.applyFocusVisiblePolyfill = applyFocusVisiblePolyfill;\n\n // Notify interested libraries of the polyfill's presence, in case the\n // polyfill was loaded lazily:\n var event;\n\n try {\n event = new CustomEvent('focus-visible-polyfill-ready');\n } catch (error) {\n // IE11 does not support using CustomEvent as a constructor directly:\n event = document.createEvent('CustomEvent');\n event.initCustomEvent('focus-visible-polyfill-ready', false, false, {});\n }\n\n window.dispatchEvent(event);\n }\n\n if (typeof document !== 'undefined') {\n // Apply the polyfill to the global document, so that no JavaScript\n // coordination is required to use the polyfill in the top-level document:\n applyFocusVisiblePolyfill(document);\n }\n\n})));\n", "(function(global) {\r\n /**\r\n * Polyfill URLSearchParams\r\n *\r\n * Inspired from : https://github.com/WebReflection/url-search-params/blob/master/src/url-search-params.js\r\n */\r\n\r\n var checkIfIteratorIsSupported = function() {\r\n try {\r\n return !!Symbol.iterator;\r\n } catch (error) {\r\n return false;\r\n }\r\n };\r\n\r\n\r\n var iteratorSupported = checkIfIteratorIsSupported();\r\n\r\n var createIterator = function(items) {\r\n var iterator = {\r\n next: function() {\r\n var value = items.shift();\r\n return { done: value === void 0, value: value };\r\n }\r\n };\r\n\r\n if (iteratorSupported) {\r\n iterator[Symbol.iterator] = function() {\r\n return iterator;\r\n };\r\n }\r\n\r\n return iterator;\r\n };\r\n\r\n /**\r\n * Search param name and values should be encoded according to https://url.spec.whatwg.org/#urlencoded-serializing\r\n * encodeURIComponent() produces the same result except encoding spaces as `%20` instead of `+`.\r\n */\r\n var serializeParam = function(value) {\r\n return encodeURIComponent(value).replace(/%20/g, '+');\r\n };\r\n\r\n var deserializeParam = function(value) {\r\n return decodeURIComponent(String(value).replace(/\\+/g, ' '));\r\n };\r\n\r\n var polyfillURLSearchParams = function() {\r\n\r\n var URLSearchParams = function(searchString) {\r\n Object.defineProperty(this, '_entries', { writable: true, value: {} });\r\n var typeofSearchString = typeof searchString;\r\n\r\n if (typeofSearchString === 'undefined') {\r\n // do nothing\r\n } else if (typeofSearchString === 'string') {\r\n if (searchString !== '') {\r\n this._fromString(searchString);\r\n }\r\n } else if (searchString instanceof URLSearchParams) {\r\n var _this = this;\r\n searchString.forEach(function(value, name) {\r\n _this.append(name, value);\r\n });\r\n } else if ((searchString !== null) && (typeofSearchString === 'object')) {\r\n if (Object.prototype.toString.call(searchString) === '[object Array]') {\r\n for (var i = 0; i < searchString.length; i++) {\r\n var entry = searchString[i];\r\n if ((Object.prototype.toString.call(entry) === '[object Array]') || (entry.length !== 2)) {\r\n this.append(entry[0], entry[1]);\r\n } else {\r\n throw new TypeError('Expected [string, any] as entry at index ' + i + ' of URLSearchParams\\'s input');\r\n }\r\n }\r\n } else {\r\n for (var key in searchString) {\r\n if (searchString.hasOwnProperty(key)) {\r\n this.append(key, searchString[key]);\r\n }\r\n }\r\n }\r\n } else {\r\n throw new TypeError('Unsupported input\\'s type for URLSearchParams');\r\n }\r\n };\r\n\r\n var proto = URLSearchParams.prototype;\r\n\r\n proto.append = function(name, value) {\r\n if (name in this._entries) {\r\n this._entries[name].push(String(value));\r\n } else {\r\n this._entries[name] = [String(value)];\r\n }\r\n };\r\n\r\n proto.delete = function(name) {\r\n delete this._entries[name];\r\n };\r\n\r\n proto.get = function(name) {\r\n return (name in this._entries) ? this._entries[name][0] : null;\r\n };\r\n\r\n proto.getAll = function(name) {\r\n return (name in this._entries) ? this._entries[name].slice(0) : [];\r\n };\r\n\r\n proto.has = function(name) {\r\n return (name in this._entries);\r\n };\r\n\r\n proto.set = function(name, value) {\r\n this._entries[name] = [String(value)];\r\n };\r\n\r\n proto.forEach = function(callback, thisArg) {\r\n var entries;\r\n for (var name in this._entries) {\r\n if (this._entries.hasOwnProperty(name)) {\r\n entries = this._entries[name];\r\n for (var i = 0; i < entries.length; i++) {\r\n callback.call(thisArg, entries[i], name, this);\r\n }\r\n }\r\n }\r\n };\r\n\r\n proto.keys = function() {\r\n var items = [];\r\n this.forEach(function(value, name) {\r\n items.push(name);\r\n });\r\n return createIterator(items);\r\n };\r\n\r\n proto.values = function() {\r\n var items = [];\r\n this.forEach(function(value) {\r\n items.push(value);\r\n });\r\n return createIterator(items);\r\n };\r\n\r\n proto.entries = function() {\r\n var items = [];\r\n this.forEach(function(value, name) {\r\n items.push([name, value]);\r\n });\r\n return createIterator(items);\r\n };\r\n\r\n if (iteratorSupported) {\r\n proto[Symbol.iterator] = proto.entries;\r\n }\r\n\r\n proto.toString = function() {\r\n var searchArray = [];\r\n this.forEach(function(value, name) {\r\n searchArray.push(serializeParam(name) + '=' + serializeParam(value));\r\n });\r\n return searchArray.join('&');\r\n };\r\n\r\n\r\n global.URLSearchParams = URLSearchParams;\r\n };\r\n\r\n var checkIfURLSearchParamsSupported = function() {\r\n try {\r\n var URLSearchParams = global.URLSearchParams;\r\n\r\n return (\r\n (new URLSearchParams('?a=1').toString() === 'a=1') &&\r\n (typeof URLSearchParams.prototype.set === 'function') &&\r\n (typeof URLSearchParams.prototype.entries === 'function')\r\n );\r\n } catch (e) {\r\n return false;\r\n }\r\n };\r\n\r\n if (!checkIfURLSearchParamsSupported()) {\r\n polyfillURLSearchParams();\r\n }\r\n\r\n var proto = global.URLSearchParams.prototype;\r\n\r\n if (typeof proto.sort !== 'function') {\r\n proto.sort = function() {\r\n var _this = this;\r\n var items = [];\r\n this.forEach(function(value, name) {\r\n items.push([name, value]);\r\n if (!_this._entries) {\r\n _this.delete(name);\r\n }\r\n });\r\n items.sort(function(a, b) {\r\n if (a[0] < b[0]) {\r\n return -1;\r\n } else if (a[0] > b[0]) {\r\n return +1;\r\n } else {\r\n return 0;\r\n }\r\n });\r\n if (_this._entries) { // force reset because IE keeps keys index\r\n _this._entries = {};\r\n }\r\n for (var i = 0; i < items.length; i++) {\r\n this.append(items[i][0], items[i][1]);\r\n }\r\n };\r\n }\r\n\r\n if (typeof proto._fromString !== 'function') {\r\n Object.defineProperty(proto, '_fromString', {\r\n enumerable: false,\r\n configurable: false,\r\n writable: false,\r\n value: function(searchString) {\r\n if (this._entries) {\r\n this._entries = {};\r\n } else {\r\n var keys = [];\r\n this.forEach(function(value, name) {\r\n keys.push(name);\r\n });\r\n for (var i = 0; i < keys.length; i++) {\r\n this.delete(keys[i]);\r\n }\r\n }\r\n\r\n searchString = searchString.replace(/^\\?/, '');\r\n var attributes = searchString.split('&');\r\n var attribute;\r\n for (var i = 0; i < attributes.length; i++) {\r\n attribute = attributes[i].split('=');\r\n this.append(\r\n deserializeParam(attribute[0]),\r\n (attribute.length > 1) ? deserializeParam(attribute[1]) : ''\r\n );\r\n }\r\n }\r\n });\r\n }\r\n\r\n // HTMLAnchorElement\r\n\r\n})(\r\n (typeof global !== 'undefined') ? global\r\n : ((typeof window !== 'undefined') ? window\r\n : ((typeof self !== 'undefined') ? self : this))\r\n);\r\n\r\n(function(global) {\r\n /**\r\n * Polyfill URL\r\n *\r\n * Inspired from : https://github.com/arv/DOM-URL-Polyfill/blob/master/src/url.js\r\n */\r\n\r\n var checkIfURLIsSupported = function() {\r\n try {\r\n var u = new global.URL('b', 'http://a');\r\n u.pathname = 'c d';\r\n return (u.href === 'http://a/c%20d') && u.searchParams;\r\n } catch (e) {\r\n return false;\r\n }\r\n };\r\n\r\n\r\n var polyfillURL = function() {\r\n var _URL = global.URL;\r\n\r\n var URL = function(url, base) {\r\n if (typeof url !== 'string') url = String(url);\r\n if (base && typeof base !== 'string') base = String(base);\r\n\r\n // Only create another document if the base is different from current location.\r\n var doc = document, baseElement;\r\n if (base && (global.location === void 0 || base !== global.location.href)) {\r\n base = base.toLowerCase();\r\n doc = document.implementation.createHTMLDocument('');\r\n baseElement = doc.createElement('base');\r\n baseElement.href = base;\r\n doc.head.appendChild(baseElement);\r\n try {\r\n if (baseElement.href.indexOf(base) !== 0) throw new Error(baseElement.href);\r\n } catch (err) {\r\n throw new Error('URL unable to set base ' + base + ' due to ' + err);\r\n }\r\n }\r\n\r\n var anchorElement = doc.createElement('a');\r\n anchorElement.href = url;\r\n if (baseElement) {\r\n doc.body.appendChild(anchorElement);\r\n anchorElement.href = anchorElement.href; // force href to refresh\r\n }\r\n\r\n var inputElement = doc.createElement('input');\r\n inputElement.type = 'url';\r\n inputElement.value = url;\r\n\r\n if (anchorElement.protocol === ':' || !/:/.test(anchorElement.href) || (!inputElement.checkValidity() && !base)) {\r\n throw new TypeError('Invalid URL');\r\n }\r\n\r\n Object.defineProperty(this, '_anchorElement', {\r\n value: anchorElement\r\n });\r\n\r\n\r\n // create a linked searchParams which reflect its changes on URL\r\n var searchParams = new global.URLSearchParams(this.search);\r\n var enableSearchUpdate = true;\r\n var enableSearchParamsUpdate = true;\r\n var _this = this;\r\n ['append', 'delete', 'set'].forEach(function(methodName) {\r\n var method = searchParams[methodName];\r\n searchParams[methodName] = function() {\r\n method.apply(searchParams, arguments);\r\n if (enableSearchUpdate) {\r\n enableSearchParamsUpdate = false;\r\n _this.search = searchParams.toString();\r\n enableSearchParamsUpdate = true;\r\n }\r\n };\r\n });\r\n\r\n Object.defineProperty(this, 'searchParams', {\r\n value: searchParams,\r\n enumerable: true\r\n });\r\n\r\n var search = void 0;\r\n Object.defineProperty(this, '_updateSearchParams', {\r\n enumerable: false,\r\n configurable: false,\r\n writable: false,\r\n value: function() {\r\n if (this.search !== search) {\r\n search = this.search;\r\n if (enableSearchParamsUpdate) {\r\n enableSearchUpdate = false;\r\n this.searchParams._fromString(this.search);\r\n enableSearchUpdate = true;\r\n }\r\n }\r\n }\r\n });\r\n };\r\n\r\n var proto = URL.prototype;\r\n\r\n var linkURLWithAnchorAttribute = function(attributeName) {\r\n Object.defineProperty(proto, attributeName, {\r\n get: function() {\r\n return this._anchorElement[attributeName];\r\n },\r\n set: function(value) {\r\n this._anchorElement[attributeName] = value;\r\n },\r\n enumerable: true\r\n });\r\n };\r\n\r\n ['hash', 'host', 'hostname', 'port', 'protocol']\r\n .forEach(function(attributeName) {\r\n linkURLWithAnchorAttribute(attributeName);\r\n });\r\n\r\n Object.defineProperty(proto, 'search', {\r\n get: function() {\r\n return this._anchorElement['search'];\r\n },\r\n set: function(value) {\r\n this._anchorElement['search'] = value;\r\n this._updateSearchParams();\r\n },\r\n enumerable: true\r\n });\r\n\r\n Object.defineProperties(proto, {\r\n\r\n 'toString': {\r\n get: function() {\r\n var _this = this;\r\n return function() {\r\n return _this.href;\r\n };\r\n }\r\n },\r\n\r\n 'href': {\r\n get: function() {\r\n return this._anchorElement.href.replace(/\\?$/, '');\r\n },\r\n set: function(value) {\r\n this._anchorElement.href = value;\r\n this._updateSearchParams();\r\n },\r\n enumerable: true\r\n },\r\n\r\n 'pathname': {\r\n get: function() {\r\n return this._anchorElement.pathname.replace(/(^\\/?)/, '/');\r\n },\r\n set: function(value) {\r\n this._anchorElement.pathname = value;\r\n },\r\n enumerable: true\r\n },\r\n\r\n 'origin': {\r\n get: function() {\r\n // get expected port from protocol\r\n var expectedPort = { 'http:': 80, 'https:': 443, 'ftp:': 21 }[this._anchorElement.protocol];\r\n // add port to origin if, expected port is different than actual port\r\n // and it is not empty f.e http://foo:8080\r\n // 8080 != 80 && 8080 != ''\r\n var addPortToOrigin = this._anchorElement.port != expectedPort &&\r\n this._anchorElement.port !== '';\r\n\r\n return this._anchorElement.protocol +\r\n '//' +\r\n this._anchorElement.hostname +\r\n (addPortToOrigin ? (':' + this._anchorElement.port) : '');\r\n },\r\n enumerable: true\r\n },\r\n\r\n 'password': { // TODO\r\n get: function() {\r\n return '';\r\n },\r\n set: function(value) {\r\n },\r\n enumerable: true\r\n },\r\n\r\n 'username': { // TODO\r\n get: function() {\r\n return '';\r\n },\r\n set: function(value) {\r\n },\r\n enumerable: true\r\n },\r\n });\r\n\r\n URL.createObjectURL = function(blob) {\r\n return _URL.createObjectURL.apply(_URL, arguments);\r\n };\r\n\r\n URL.revokeObjectURL = function(url) {\r\n return _URL.revokeObjectURL.apply(_URL, arguments);\r\n };\r\n\r\n global.URL = URL;\r\n\r\n };\r\n\r\n if (!checkIfURLIsSupported()) {\r\n polyfillURL();\r\n }\r\n\r\n if ((global.location !== void 0) && !('origin' in global.location)) {\r\n var getOrigin = function() {\r\n return global.location.protocol + '//' + global.location.hostname + (global.location.port ? (':' + global.location.port) : '');\r\n };\r\n\r\n try {\r\n Object.defineProperty(global.location, 'origin', {\r\n get: getOrigin,\r\n enumerable: true\r\n });\r\n } catch (e) {\r\n setInterval(function() {\r\n global.location.origin = getOrigin();\r\n }, 100);\r\n }\r\n }\r\n\r\n})(\r\n (typeof global !== 'undefined') ? global\r\n : ((typeof window !== 'undefined') ? window\r\n : ((typeof self !== 'undefined') ? self : this))\r\n);\r\n", "/*! *****************************************************************************\r\nCopyright (c) Microsoft Corporation.\r\n\r\nPermission to use, copy, modify, and/or distribute this software for any\r\npurpose with or without fee is hereby granted.\r\n\r\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\r\nREGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY\r\nAND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\r\nINDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM\r\nLOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR\r\nOTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR\r\nPERFORMANCE OF THIS SOFTWARE.\r\n***************************************************************************** */\r\n/* global global, define, System, Reflect, Promise */\r\nvar __extends;\r\nvar __assign;\r\nvar __rest;\r\nvar __decorate;\r\nvar __param;\r\nvar __metadata;\r\nvar __awaiter;\r\nvar __generator;\r\nvar __exportStar;\r\nvar __values;\r\nvar __read;\r\nvar __spread;\r\nvar __spreadArrays;\r\nvar __spreadArray;\r\nvar __await;\r\nvar __asyncGenerator;\r\nvar __asyncDelegator;\r\nvar __asyncValues;\r\nvar __makeTemplateObject;\r\nvar __importStar;\r\nvar __importDefault;\r\nvar __classPrivateFieldGet;\r\nvar __classPrivateFieldSet;\r\nvar __createBinding;\r\n(function (factory) {\r\n var root = typeof global === \"object\" ? global : typeof self === \"object\" ? self : typeof this === \"object\" ? this : {};\r\n if (typeof define === \"function\" && define.amd) {\r\n define(\"tslib\", [\"exports\"], function (exports) { factory(createExporter(root, createExporter(exports))); });\r\n }\r\n else if (typeof module === \"object\" && typeof module.exports === \"object\") {\r\n factory(createExporter(root, createExporter(module.exports)));\r\n }\r\n else {\r\n factory(createExporter(root));\r\n }\r\n function createExporter(exports, previous) {\r\n if (exports !== root) {\r\n if (typeof Object.create === \"function\") {\r\n Object.defineProperty(exports, \"__esModule\", { value: true });\r\n }\r\n else {\r\n exports.__esModule = true;\r\n }\r\n }\r\n return function (id, v) { return exports[id] = previous ? previous(id, v) : v; };\r\n }\r\n})\r\n(function (exporter) {\r\n var extendStatics = Object.setPrototypeOf ||\r\n ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||\r\n function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };\r\n\r\n __extends = function (d, b) {\r\n if (typeof b !== \"function\" && b !== null)\r\n throw new TypeError(\"Class extends value \" + String(b) + \" is not a constructor or null\");\r\n extendStatics(d, b);\r\n function __() { this.constructor = d; }\r\n d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());\r\n };\r\n\r\n __assign = Object.assign || function (t) {\r\n for (var s, i = 1, n = arguments.length; i < n; i++) {\r\n s = arguments[i];\r\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];\r\n }\r\n return t;\r\n };\r\n\r\n __rest = function (s, e) {\r\n var t = {};\r\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)\r\n t[p] = s[p];\r\n if (s != null && typeof Object.getOwnPropertySymbols === \"function\")\r\n for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {\r\n if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))\r\n t[p[i]] = s[p[i]];\r\n }\r\n return t;\r\n };\r\n\r\n __decorate = function (decorators, target, key, desc) {\r\n var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;\r\n if (typeof Reflect === \"object\" && typeof Reflect.decorate === \"function\") r = Reflect.decorate(decorators, target, key, desc);\r\n else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;\r\n return c > 3 && r && Object.defineProperty(target, key, r), r;\r\n };\r\n\r\n __param = function (paramIndex, decorator) {\r\n return function (target, key) { decorator(target, key, paramIndex); }\r\n };\r\n\r\n __metadata = function (metadataKey, metadataValue) {\r\n if (typeof Reflect === \"object\" && typeof Reflect.metadata === \"function\") return Reflect.metadata(metadataKey, metadataValue);\r\n };\r\n\r\n __awaiter = function (thisArg, _arguments, P, generator) {\r\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\r\n return new (P || (P = Promise))(function (resolve, reject) {\r\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\r\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\r\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\r\n step((generator = generator.apply(thisArg, _arguments || [])).next());\r\n });\r\n };\r\n\r\n __generator = function (thisArg, body) {\r\n var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;\r\n return g = { next: verb(0), \"throw\": verb(1), \"return\": verb(2) }, typeof Symbol === \"function\" && (g[Symbol.iterator] = function() { return this; }), g;\r\n function verb(n) { return function (v) { return step([n, v]); }; }\r\n function step(op) {\r\n if (f) throw new TypeError(\"Generator is already executing.\");\r\n while (_) try {\r\n if (f = 1, y && (t = op[0] & 2 ? y[\"return\"] : op[0] ? y[\"throw\"] || ((t = y[\"return\"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;\r\n if (y = 0, t) op = [op[0] & 2, t.value];\r\n switch (op[0]) {\r\n case 0: case 1: t = op; break;\r\n case 4: _.label++; return { value: op[1], done: false };\r\n case 5: _.label++; y = op[1]; op = [0]; continue;\r\n case 7: op = _.ops.pop(); _.trys.pop(); continue;\r\n default:\r\n if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }\r\n if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }\r\n if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }\r\n if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }\r\n if (t[2]) _.ops.pop();\r\n _.trys.pop(); continue;\r\n }\r\n op = body.call(thisArg, _);\r\n } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }\r\n if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };\r\n }\r\n };\r\n\r\n __exportStar = function(m, o) {\r\n for (var p in m) if (p !== \"default\" && !Object.prototype.hasOwnProperty.call(o, p)) __createBinding(o, m, p);\r\n };\r\n\r\n __createBinding = Object.create ? (function(o, m, k, k2) {\r\n if (k2 === undefined) k2 = k;\r\n Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });\r\n }) : (function(o, m, k, k2) {\r\n if (k2 === undefined) k2 = k;\r\n o[k2] = m[k];\r\n });\r\n\r\n __values = function (o) {\r\n var s = typeof Symbol === \"function\" && Symbol.iterator, m = s && o[s], i = 0;\r\n if (m) return m.call(o);\r\n if (o && typeof o.length === \"number\") return {\r\n next: function () {\r\n if (o && i >= o.length) o = void 0;\r\n return { value: o && o[i++], done: !o };\r\n }\r\n };\r\n throw new TypeError(s ? \"Object is not iterable.\" : \"Symbol.iterator is not defined.\");\r\n };\r\n\r\n __read = function (o, n) {\r\n var m = typeof Symbol === \"function\" && o[Symbol.iterator];\r\n if (!m) return o;\r\n var i = m.call(o), r, ar = [], e;\r\n try {\r\n while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);\r\n }\r\n catch (error) { e = { error: error }; }\r\n finally {\r\n try {\r\n if (r && !r.done && (m = i[\"return\"])) m.call(i);\r\n }\r\n finally { if (e) throw e.error; }\r\n }\r\n return ar;\r\n };\r\n\r\n /** @deprecated */\r\n __spread = function () {\r\n for (var ar = [], i = 0; i < arguments.length; i++)\r\n ar = ar.concat(__read(arguments[i]));\r\n return ar;\r\n };\r\n\r\n /** @deprecated */\r\n __spreadArrays = function () {\r\n for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;\r\n for (var r = Array(s), k = 0, i = 0; i < il; i++)\r\n for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)\r\n r[k] = a[j];\r\n return r;\r\n };\r\n\r\n __spreadArray = function (to, from, pack) {\r\n if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {\r\n if (ar || !(i in from)) {\r\n if (!ar) ar = Array.prototype.slice.call(from, 0, i);\r\n ar[i] = from[i];\r\n }\r\n }\r\n return to.concat(ar || Array.prototype.slice.call(from));\r\n };\r\n\r\n __await = function (v) {\r\n return this instanceof __await ? (this.v = v, this) : new __await(v);\r\n };\r\n\r\n __asyncGenerator = function (thisArg, _arguments, generator) {\r\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\r\n var g = generator.apply(thisArg, _arguments || []), i, q = [];\r\n return i = {}, verb(\"next\"), verb(\"throw\"), verb(\"return\"), i[Symbol.asyncIterator] = function () { return this; }, i;\r\n function verb(n) { if (g[n]) i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; }\r\n function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }\r\n function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }\r\n function fulfill(value) { resume(\"next\", value); }\r\n function reject(value) { resume(\"throw\", value); }\r\n function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }\r\n };\r\n\r\n __asyncDelegator = function (o) {\r\n var i, p;\r\n return i = {}, verb(\"next\"), verb(\"throw\", function (e) { throw e; }), verb(\"return\"), i[Symbol.iterator] = function () { return this; }, i;\r\n function verb(n, f) { i[n] = o[n] ? function (v) { return (p = !p) ? { value: __await(o[n](v)), done: n === \"return\" } : f ? f(v) : v; } : f; }\r\n };\r\n\r\n __asyncValues = function (o) {\r\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\r\n var m = o[Symbol.asyncIterator], i;\r\n return m ? m.call(o) : (o = typeof __values === \"function\" ? __values(o) : o[Symbol.iterator](), i = {}, verb(\"next\"), verb(\"throw\"), verb(\"return\"), i[Symbol.asyncIterator] = function () { return this; }, i);\r\n function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }\r\n function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }\r\n };\r\n\r\n __makeTemplateObject = function (cooked, raw) {\r\n if (Object.defineProperty) { Object.defineProperty(cooked, \"raw\", { value: raw }); } else { cooked.raw = raw; }\r\n return cooked;\r\n };\r\n\r\n var __setModuleDefault = Object.create ? (function(o, v) {\r\n Object.defineProperty(o, \"default\", { enumerable: true, value: v });\r\n }) : function(o, v) {\r\n o[\"default\"] = v;\r\n };\r\n\r\n __importStar = function (mod) {\r\n if (mod && mod.__esModule) return mod;\r\n var result = {};\r\n if (mod != null) for (var k in mod) if (k !== \"default\" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);\r\n __setModuleDefault(result, mod);\r\n return result;\r\n };\r\n\r\n __importDefault = function (mod) {\r\n return (mod && mod.__esModule) ? mod : { \"default\": mod };\r\n };\r\n\r\n __classPrivateFieldGet = function (receiver, state, kind, f) {\r\n if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a getter\");\r\n if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot read private member from an object whose class did not declare it\");\r\n return kind === \"m\" ? f : kind === \"a\" ? f.call(receiver) : f ? f.value : state.get(receiver);\r\n };\r\n\r\n __classPrivateFieldSet = function (receiver, state, value, kind, f) {\r\n if (kind === \"m\") throw new TypeError(\"Private method is not writable\");\r\n if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a setter\");\r\n if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot write private member to an object whose class did not declare it\");\r\n return (kind === \"a\" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;\r\n };\r\n\r\n exporter(\"__extends\", __extends);\r\n exporter(\"__assign\", __assign);\r\n exporter(\"__rest\", __rest);\r\n exporter(\"__decorate\", __decorate);\r\n exporter(\"__param\", __param);\r\n exporter(\"__metadata\", __metadata);\r\n exporter(\"__awaiter\", __awaiter);\r\n exporter(\"__generator\", __generator);\r\n exporter(\"__exportStar\", __exportStar);\r\n exporter(\"__createBinding\", __createBinding);\r\n exporter(\"__values\", __values);\r\n exporter(\"__read\", __read);\r\n exporter(\"__spread\", __spread);\r\n exporter(\"__spreadArrays\", __spreadArrays);\r\n exporter(\"__spreadArray\", __spreadArray);\r\n exporter(\"__await\", __await);\r\n exporter(\"__asyncGenerator\", __asyncGenerator);\r\n exporter(\"__asyncDelegator\", __asyncDelegator);\r\n exporter(\"__asyncValues\", __asyncValues);\r\n exporter(\"__makeTemplateObject\", __makeTemplateObject);\r\n exporter(\"__importStar\", __importStar);\r\n exporter(\"__importDefault\", __importDefault);\r\n exporter(\"__classPrivateFieldGet\", __classPrivateFieldGet);\r\n exporter(\"__classPrivateFieldSet\", __classPrivateFieldSet);\r\n});\r\n", "/*!\n * clipboard.js v2.0.10\n * https://clipboardjs.com/\n *\n * Licensed MIT \u00A9 Zeno Rocha\n */\n(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"ClipboardJS\"] = factory();\n\telse\n\t\troot[\"ClipboardJS\"] = factory();\n})(this, function() {\nreturn /******/ (function() { // webpackBootstrap\n/******/ \tvar __webpack_modules__ = ({\n\n/***/ 686:\n/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {\n\n\"use strict\";\n\n// EXPORTS\n__webpack_require__.d(__webpack_exports__, {\n \"default\": function() { return /* binding */ clipboard; }\n});\n\n// EXTERNAL MODULE: ./node_modules/tiny-emitter/index.js\nvar tiny_emitter = __webpack_require__(279);\nvar tiny_emitter_default = /*#__PURE__*/__webpack_require__.n(tiny_emitter);\n// EXTERNAL MODULE: ./node_modules/good-listener/src/listen.js\nvar listen = __webpack_require__(370);\nvar listen_default = /*#__PURE__*/__webpack_require__.n(listen);\n// EXTERNAL MODULE: ./node_modules/select/src/select.js\nvar src_select = __webpack_require__(817);\nvar select_default = /*#__PURE__*/__webpack_require__.n(src_select);\n;// CONCATENATED MODULE: ./src/common/command.js\n/**\n * Executes a given operation type.\n * @param {String} type\n * @return {Boolean}\n */\nfunction command(type) {\n try {\n return document.execCommand(type);\n } catch (err) {\n return false;\n }\n}\n;// CONCATENATED MODULE: ./src/actions/cut.js\n\n\n/**\n * Cut action wrapper.\n * @param {String|HTMLElement} target\n * @return {String}\n */\n\nvar ClipboardActionCut = function ClipboardActionCut(target) {\n var selectedText = select_default()(target);\n command('cut');\n return selectedText;\n};\n\n/* harmony default export */ var actions_cut = (ClipboardActionCut);\n;// CONCATENATED MODULE: ./src/common/create-fake-element.js\n/**\n * Creates a fake textarea element with a value.\n * @param {String} value\n * @return {HTMLElement}\n */\nfunction createFakeElement(value) {\n var isRTL = document.documentElement.getAttribute('dir') === 'rtl';\n var fakeElement = document.createElement('textarea'); // Prevent zooming on iOS\n\n fakeElement.style.fontSize = '12pt'; // Reset box model\n\n fakeElement.style.border = '0';\n fakeElement.style.padding = '0';\n fakeElement.style.margin = '0'; // Move element out of screen horizontally\n\n fakeElement.style.position = 'absolute';\n fakeElement.style[isRTL ? 'right' : 'left'] = '-9999px'; // Move element to the same position vertically\n\n var yPosition = window.pageYOffset || document.documentElement.scrollTop;\n fakeElement.style.top = \"\".concat(yPosition, \"px\");\n fakeElement.setAttribute('readonly', '');\n fakeElement.value = value;\n return fakeElement;\n}\n;// CONCATENATED MODULE: ./src/actions/copy.js\n\n\n\n/**\n * Copy action wrapper.\n * @param {String|HTMLElement} target\n * @param {Object} options\n * @return {String}\n */\n\nvar ClipboardActionCopy = function ClipboardActionCopy(target) {\n var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {\n container: document.body\n };\n var selectedText = '';\n\n if (typeof target === 'string') {\n var fakeElement = createFakeElement(target);\n options.container.appendChild(fakeElement);\n selectedText = select_default()(fakeElement);\n command('copy');\n fakeElement.remove();\n } else {\n selectedText = select_default()(target);\n command('copy');\n }\n\n return selectedText;\n};\n\n/* harmony default export */ var actions_copy = (ClipboardActionCopy);\n;// CONCATENATED MODULE: ./src/actions/default.js\nfunction _typeof(obj) { \"@babel/helpers - typeof\"; if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return _typeof(obj); }\n\n\n\n/**\n * Inner function which performs selection from either `text` or `target`\n * properties and then executes copy or cut operations.\n * @param {Object} options\n */\n\nvar ClipboardActionDefault = function ClipboardActionDefault() {\n var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n // Defines base properties passed from constructor.\n var _options$action = options.action,\n action = _options$action === void 0 ? 'copy' : _options$action,\n container = options.container,\n target = options.target,\n text = options.text; // Sets the `action` to be performed which can be either 'copy' or 'cut'.\n\n if (action !== 'copy' && action !== 'cut') {\n throw new Error('Invalid \"action\" value, use either \"copy\" or \"cut\"');\n } // Sets the `target` property using an element that will be have its content copied.\n\n\n if (target !== undefined) {\n if (target && _typeof(target) === 'object' && target.nodeType === 1) {\n if (action === 'copy' && target.hasAttribute('disabled')) {\n throw new Error('Invalid \"target\" attribute. Please use \"readonly\" instead of \"disabled\" attribute');\n }\n\n if (action === 'cut' && (target.hasAttribute('readonly') || target.hasAttribute('disabled'))) {\n throw new Error('Invalid \"target\" attribute. You can\\'t cut text from elements with \"readonly\" or \"disabled\" attributes');\n }\n } else {\n throw new Error('Invalid \"target\" value, use a valid Element');\n }\n } // Define selection strategy based on `text` property.\n\n\n if (text) {\n return actions_copy(text, {\n container: container\n });\n } // Defines which selection strategy based on `target` property.\n\n\n if (target) {\n return action === 'cut' ? actions_cut(target) : actions_copy(target, {\n container: container\n });\n }\n};\n\n/* harmony default export */ var actions_default = (ClipboardActionDefault);\n;// CONCATENATED MODULE: ./src/clipboard.js\nfunction clipboard_typeof(obj) { \"@babel/helpers - typeof\"; if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { clipboard_typeof = function _typeof(obj) { return typeof obj; }; } else { clipboard_typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return clipboard_typeof(obj); }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }\n\nfunction _inherits(subClass, superClass) { if (typeof superClass !== \"function\" && superClass !== null) { throw new TypeError(\"Super expression must either be null or a function\"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }\n\nfunction _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }\n\nfunction _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }\n\nfunction _possibleConstructorReturn(self, call) { if (call && (clipboard_typeof(call) === \"object\" || typeof call === \"function\")) { return call; } return _assertThisInitialized(self); }\n\nfunction _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\"); } return self; }\n\nfunction _isNativeReflectConstruct() { if (typeof Reflect === \"undefined\" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === \"function\") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } }\n\nfunction _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }\n\n\n\n\n\n\n/**\n * Helper function to retrieve attribute value.\n * @param {String} suffix\n * @param {Element} element\n */\n\nfunction getAttributeValue(suffix, element) {\n var attribute = \"data-clipboard-\".concat(suffix);\n\n if (!element.hasAttribute(attribute)) {\n return;\n }\n\n return element.getAttribute(attribute);\n}\n/**\n * Base class which takes one or more elements, adds event listeners to them,\n * and instantiates a new `ClipboardAction` on each click.\n */\n\n\nvar Clipboard = /*#__PURE__*/function (_Emitter) {\n _inherits(Clipboard, _Emitter);\n\n var _super = _createSuper(Clipboard);\n\n /**\n * @param {String|HTMLElement|HTMLCollection|NodeList} trigger\n * @param {Object} options\n */\n function Clipboard(trigger, options) {\n var _this;\n\n _classCallCheck(this, Clipboard);\n\n _this = _super.call(this);\n\n _this.resolveOptions(options);\n\n _this.listenClick(trigger);\n\n return _this;\n }\n /**\n * Defines if attributes would be resolved using internal setter functions\n * or custom functions that were passed in the constructor.\n * @param {Object} options\n */\n\n\n _createClass(Clipboard, [{\n key: \"resolveOptions\",\n value: function resolveOptions() {\n var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n this.action = typeof options.action === 'function' ? options.action : this.defaultAction;\n this.target = typeof options.target === 'function' ? options.target : this.defaultTarget;\n this.text = typeof options.text === 'function' ? options.text : this.defaultText;\n this.container = clipboard_typeof(options.container) === 'object' ? options.container : document.body;\n }\n /**\n * Adds a click event listener to the passed trigger.\n * @param {String|HTMLElement|HTMLCollection|NodeList} trigger\n */\n\n }, {\n key: \"listenClick\",\n value: function listenClick(trigger) {\n var _this2 = this;\n\n this.listener = listen_default()(trigger, 'click', function (e) {\n return _this2.onClick(e);\n });\n }\n /**\n * Defines a new `ClipboardAction` on each click event.\n * @param {Event} e\n */\n\n }, {\n key: \"onClick\",\n value: function onClick(e) {\n var trigger = e.delegateTarget || e.currentTarget;\n var action = this.action(trigger) || 'copy';\n var text = actions_default({\n action: action,\n container: this.container,\n target: this.target(trigger),\n text: this.text(trigger)\n }); // Fires an event based on the copy operation result.\n\n this.emit(text ? 'success' : 'error', {\n action: action,\n text: text,\n trigger: trigger,\n clearSelection: function clearSelection() {\n if (trigger) {\n trigger.focus();\n }\n\n document.activeElement.blur();\n window.getSelection().removeAllRanges();\n }\n });\n }\n /**\n * Default `action` lookup function.\n * @param {Element} trigger\n */\n\n }, {\n key: \"defaultAction\",\n value: function defaultAction(trigger) {\n return getAttributeValue('action', trigger);\n }\n /**\n * Default `target` lookup function.\n * @param {Element} trigger\n */\n\n }, {\n key: \"defaultTarget\",\n value: function defaultTarget(trigger) {\n var selector = getAttributeValue('target', trigger);\n\n if (selector) {\n return document.querySelector(selector);\n }\n }\n /**\n * Allow fire programmatically a copy action\n * @param {String|HTMLElement} target\n * @param {Object} options\n * @returns Text copied.\n */\n\n }, {\n key: \"defaultText\",\n\n /**\n * Default `text` lookup function.\n * @param {Element} trigger\n */\n value: function defaultText(trigger) {\n return getAttributeValue('text', trigger);\n }\n /**\n * Destroy lifecycle.\n */\n\n }, {\n key: \"destroy\",\n value: function destroy() {\n this.listener.destroy();\n }\n }], [{\n key: \"copy\",\n value: function copy(target) {\n var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {\n container: document.body\n };\n return actions_copy(target, options);\n }\n /**\n * Allow fire programmatically a cut action\n * @param {String|HTMLElement} target\n * @returns Text cutted.\n */\n\n }, {\n key: \"cut\",\n value: function cut(target) {\n return actions_cut(target);\n }\n /**\n * Returns the support of the given action, or all actions if no action is\n * given.\n * @param {String} [action]\n */\n\n }, {\n key: \"isSupported\",\n value: function isSupported() {\n var action = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ['copy', 'cut'];\n var actions = typeof action === 'string' ? [action] : action;\n var support = !!document.queryCommandSupported;\n actions.forEach(function (action) {\n support = support && !!document.queryCommandSupported(action);\n });\n return support;\n }\n }]);\n\n return Clipboard;\n}((tiny_emitter_default()));\n\n/* harmony default export */ var clipboard = (Clipboard);\n\n/***/ }),\n\n/***/ 828:\n/***/ (function(module) {\n\nvar DOCUMENT_NODE_TYPE = 9;\n\n/**\n * A polyfill for Element.matches()\n */\nif (typeof Element !== 'undefined' && !Element.prototype.matches) {\n var proto = Element.prototype;\n\n proto.matches = proto.matchesSelector ||\n proto.mozMatchesSelector ||\n proto.msMatchesSelector ||\n proto.oMatchesSelector ||\n proto.webkitMatchesSelector;\n}\n\n/**\n * Finds the closest parent that matches a selector.\n *\n * @param {Element} element\n * @param {String} selector\n * @return {Function}\n */\nfunction closest (element, selector) {\n while (element && element.nodeType !== DOCUMENT_NODE_TYPE) {\n if (typeof element.matches === 'function' &&\n element.matches(selector)) {\n return element;\n }\n element = element.parentNode;\n }\n}\n\nmodule.exports = closest;\n\n\n/***/ }),\n\n/***/ 438:\n/***/ (function(module, __unused_webpack_exports, __webpack_require__) {\n\nvar closest = __webpack_require__(828);\n\n/**\n * Delegates event to a selector.\n *\n * @param {Element} element\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @param {Boolean} useCapture\n * @return {Object}\n */\nfunction _delegate(element, selector, type, callback, useCapture) {\n var listenerFn = listener.apply(this, arguments);\n\n element.addEventListener(type, listenerFn, useCapture);\n\n return {\n destroy: function() {\n element.removeEventListener(type, listenerFn, useCapture);\n }\n }\n}\n\n/**\n * Delegates event to a selector.\n *\n * @param {Element|String|Array} [elements]\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @param {Boolean} useCapture\n * @return {Object}\n */\nfunction delegate(elements, selector, type, callback, useCapture) {\n // Handle the regular Element usage\n if (typeof elements.addEventListener === 'function') {\n return _delegate.apply(null, arguments);\n }\n\n // Handle Element-less usage, it defaults to global delegation\n if (typeof type === 'function') {\n // Use `document` as the first parameter, then apply arguments\n // This is a short way to .unshift `arguments` without running into deoptimizations\n return _delegate.bind(null, document).apply(null, arguments);\n }\n\n // Handle Selector-based usage\n if (typeof elements === 'string') {\n elements = document.querySelectorAll(elements);\n }\n\n // Handle Array-like based usage\n return Array.prototype.map.call(elements, function (element) {\n return _delegate(element, selector, type, callback, useCapture);\n });\n}\n\n/**\n * Finds closest match and invokes callback.\n *\n * @param {Element} element\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @return {Function}\n */\nfunction listener(element, selector, type, callback) {\n return function(e) {\n e.delegateTarget = closest(e.target, selector);\n\n if (e.delegateTarget) {\n callback.call(element, e);\n }\n }\n}\n\nmodule.exports = delegate;\n\n\n/***/ }),\n\n/***/ 879:\n/***/ (function(__unused_webpack_module, exports) {\n\n/**\n * Check if argument is a HTML element.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.node = function(value) {\n return value !== undefined\n && value instanceof HTMLElement\n && value.nodeType === 1;\n};\n\n/**\n * Check if argument is a list of HTML elements.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.nodeList = function(value) {\n var type = Object.prototype.toString.call(value);\n\n return value !== undefined\n && (type === '[object NodeList]' || type === '[object HTMLCollection]')\n && ('length' in value)\n && (value.length === 0 || exports.node(value[0]));\n};\n\n/**\n * Check if argument is a string.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.string = function(value) {\n return typeof value === 'string'\n || value instanceof String;\n};\n\n/**\n * Check if argument is a function.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.fn = function(value) {\n var type = Object.prototype.toString.call(value);\n\n return type === '[object Function]';\n};\n\n\n/***/ }),\n\n/***/ 370:\n/***/ (function(module, __unused_webpack_exports, __webpack_require__) {\n\nvar is = __webpack_require__(879);\nvar delegate = __webpack_require__(438);\n\n/**\n * Validates all params and calls the right\n * listener function based on its target type.\n *\n * @param {String|HTMLElement|HTMLCollection|NodeList} target\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listen(target, type, callback) {\n if (!target && !type && !callback) {\n throw new Error('Missing required arguments');\n }\n\n if (!is.string(type)) {\n throw new TypeError('Second argument must be a String');\n }\n\n if (!is.fn(callback)) {\n throw new TypeError('Third argument must be a Function');\n }\n\n if (is.node(target)) {\n return listenNode(target, type, callback);\n }\n else if (is.nodeList(target)) {\n return listenNodeList(target, type, callback);\n }\n else if (is.string(target)) {\n return listenSelector(target, type, callback);\n }\n else {\n throw new TypeError('First argument must be a String, HTMLElement, HTMLCollection, or NodeList');\n }\n}\n\n/**\n * Adds an event listener to a HTML element\n * and returns a remove listener function.\n *\n * @param {HTMLElement} node\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenNode(node, type, callback) {\n node.addEventListener(type, callback);\n\n return {\n destroy: function() {\n node.removeEventListener(type, callback);\n }\n }\n}\n\n/**\n * Add an event listener to a list of HTML elements\n * and returns a remove listener function.\n *\n * @param {NodeList|HTMLCollection} nodeList\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenNodeList(nodeList, type, callback) {\n Array.prototype.forEach.call(nodeList, function(node) {\n node.addEventListener(type, callback);\n });\n\n return {\n destroy: function() {\n Array.prototype.forEach.call(nodeList, function(node) {\n node.removeEventListener(type, callback);\n });\n }\n }\n}\n\n/**\n * Add an event listener to a selector\n * and returns a remove listener function.\n *\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenSelector(selector, type, callback) {\n return delegate(document.body, selector, type, callback);\n}\n\nmodule.exports = listen;\n\n\n/***/ }),\n\n/***/ 817:\n/***/ (function(module) {\n\nfunction select(element) {\n var selectedText;\n\n if (element.nodeName === 'SELECT') {\n element.focus();\n\n selectedText = element.value;\n }\n else if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {\n var isReadOnly = element.hasAttribute('readonly');\n\n if (!isReadOnly) {\n element.setAttribute('readonly', '');\n }\n\n element.select();\n element.setSelectionRange(0, element.value.length);\n\n if (!isReadOnly) {\n element.removeAttribute('readonly');\n }\n\n selectedText = element.value;\n }\n else {\n if (element.hasAttribute('contenteditable')) {\n element.focus();\n }\n\n var selection = window.getSelection();\n var range = document.createRange();\n\n range.selectNodeContents(element);\n selection.removeAllRanges();\n selection.addRange(range);\n\n selectedText = selection.toString();\n }\n\n return selectedText;\n}\n\nmodule.exports = select;\n\n\n/***/ }),\n\n/***/ 279:\n/***/ (function(module) {\n\nfunction E () {\n // Keep this empty so it's easier to inherit from\n // (via https://github.com/lipsmack from https://github.com/scottcorgan/tiny-emitter/issues/3)\n}\n\nE.prototype = {\n on: function (name, callback, ctx) {\n var e = this.e || (this.e = {});\n\n (e[name] || (e[name] = [])).push({\n fn: callback,\n ctx: ctx\n });\n\n return this;\n },\n\n once: function (name, callback, ctx) {\n var self = this;\n function listener () {\n self.off(name, listener);\n callback.apply(ctx, arguments);\n };\n\n listener._ = callback\n return this.on(name, listener, ctx);\n },\n\n emit: function (name) {\n var data = [].slice.call(arguments, 1);\n var evtArr = ((this.e || (this.e = {}))[name] || []).slice();\n var i = 0;\n var len = evtArr.length;\n\n for (i; i < len; i++) {\n evtArr[i].fn.apply(evtArr[i].ctx, data);\n }\n\n return this;\n },\n\n off: function (name, callback) {\n var e = this.e || (this.e = {});\n var evts = e[name];\n var liveEvents = [];\n\n if (evts && callback) {\n for (var i = 0, len = evts.length; i < len; i++) {\n if (evts[i].fn !== callback && evts[i].fn._ !== callback)\n liveEvents.push(evts[i]);\n }\n }\n\n // Remove event from queue to prevent memory leak\n // Suggested by https://github.com/lazd\n // Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910\n\n (liveEvents.length)\n ? e[name] = liveEvents\n : delete e[name];\n\n return this;\n }\n};\n\nmodule.exports = E;\nmodule.exports.TinyEmitter = E;\n\n\n/***/ })\n\n/******/ \t});\n/************************************************************************/\n/******/ \t// The module cache\n/******/ \tvar __webpack_module_cache__ = {};\n/******/ \t\n/******/ \t// The require function\n/******/ \tfunction __webpack_require__(moduleId) {\n/******/ \t\t// Check if module is in cache\n/******/ \t\tif(__webpack_module_cache__[moduleId]) {\n/******/ \t\t\treturn __webpack_module_cache__[moduleId].exports;\n/******/ \t\t}\n/******/ \t\t// Create a new module (and put it into the cache)\n/******/ \t\tvar module = __webpack_module_cache__[moduleId] = {\n/******/ \t\t\t// no module.id needed\n/******/ \t\t\t// no module.loaded needed\n/******/ \t\t\texports: {}\n/******/ \t\t};\n/******/ \t\n/******/ \t\t// Execute the module function\n/******/ \t\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n/******/ \t\n/******/ \t\t// Return the exports of the module\n/******/ \t\treturn module.exports;\n/******/ \t}\n/******/ \t\n/************************************************************************/\n/******/ \t/* webpack/runtime/compat get default export */\n/******/ \t!function() {\n/******/ \t\t// getDefaultExport function for compatibility with non-harmony modules\n/******/ \t\t__webpack_require__.n = function(module) {\n/******/ \t\t\tvar getter = module && module.__esModule ?\n/******/ \t\t\t\tfunction() { return module['default']; } :\n/******/ \t\t\t\tfunction() { return module; };\n/******/ \t\t\t__webpack_require__.d(getter, { a: getter });\n/******/ \t\t\treturn getter;\n/******/ \t\t};\n/******/ \t}();\n/******/ \t\n/******/ \t/* webpack/runtime/define property getters */\n/******/ \t!function() {\n/******/ \t\t// define getter functions for harmony exports\n/******/ \t\t__webpack_require__.d = function(exports, definition) {\n/******/ \t\t\tfor(var key in definition) {\n/******/ \t\t\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n/******/ \t\t\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n/******/ \t\t\t\t}\n/******/ \t\t\t}\n/******/ \t\t};\n/******/ \t}();\n/******/ \t\n/******/ \t/* webpack/runtime/hasOwnProperty shorthand */\n/******/ \t!function() {\n/******/ \t\t__webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }\n/******/ \t}();\n/******/ \t\n/************************************************************************/\n/******/ \t// module exports must be returned from runtime so entry inlining is disabled\n/******/ \t// startup\n/******/ \t// Load entry module and return exports\n/******/ \treturn __webpack_require__(686);\n/******/ })()\n.default;\n});", "/*!\n * escape-html\n * Copyright(c) 2012-2013 TJ Holowaychuk\n * Copyright(c) 2015 Andreas Lubbe\n * Copyright(c) 2015 Tiancheng \"Timothy\" Gu\n * MIT Licensed\n */\n\n'use strict';\n\n/**\n * Module variables.\n * @private\n */\n\nvar matchHtmlRegExp = /[\"'&<>]/;\n\n/**\n * Module exports.\n * @public\n */\n\nmodule.exports = escapeHtml;\n\n/**\n * Escape special characters in the given string of html.\n *\n * @param {string} string The string to escape for inserting into HTML\n * @return {string}\n * @public\n */\n\nfunction escapeHtml(string) {\n var str = '' + string;\n var match = matchHtmlRegExp.exec(str);\n\n if (!match) {\n return str;\n }\n\n var escape;\n var html = '';\n var index = 0;\n var lastIndex = 0;\n\n for (index = match.index; index < str.length; index++) {\n switch (str.charCodeAt(index)) {\n case 34: // \"\n escape = '"';\n break;\n case 38: // &\n escape = '&';\n break;\n case 39: // '\n escape = ''';\n break;\n case 60: // <\n escape = '<';\n break;\n case 62: // >\n escape = '>';\n break;\n default:\n continue;\n }\n\n if (lastIndex !== index) {\n html += str.substring(lastIndex, index);\n }\n\n lastIndex = index + 1;\n html += escape;\n }\n\n return lastIndex !== index\n ? html + str.substring(lastIndex, index)\n : html;\n}\n", "Array.prototype.flat||Object.defineProperty(Array.prototype,\"flat\",{configurable:!0,value:function r(){var t=isNaN(arguments[0])?1:Number(arguments[0]);return t?Array.prototype.reduce.call(this,function(a,e){return Array.isArray(e)?a.push.apply(a,r.call(e,t-1)):a.push(e),a},[]):Array.prototype.slice.call(this)},writable:!0}),Array.prototype.flatMap||Object.defineProperty(Array.prototype,\"flatMap\",{configurable:!0,value:function(r){return Array.prototype.map.apply(this,arguments).flat()},writable:!0})\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport \"array-flat-polyfill\"\nimport \"focus-visible\"\nimport \"unfetch/polyfill\"\nimport \"url-polyfill\"\n\nimport {\n EMPTY,\n NEVER,\n Subject,\n defer,\n delay,\n filter,\n map,\n merge,\n mergeWith,\n shareReplay,\n switchMap\n} from \"rxjs\"\n\nimport { configuration, feature } from \"./_\"\nimport {\n at,\n getOptionalElement,\n requestJSON,\n setToggle,\n watchDocument,\n watchKeyboard,\n watchLocation,\n watchLocationTarget,\n watchMedia,\n watchPrint,\n watchViewport\n} from \"./browser\"\nimport {\n getComponentElement,\n getComponentElements,\n mountBackToTop,\n mountContent,\n mountDialog,\n mountHeader,\n mountHeaderTitle,\n mountPalette,\n mountSearch,\n mountSearchHiglight,\n mountSidebar,\n mountSource,\n mountTableOfContents,\n mountTabs,\n watchHeader,\n watchMain\n} from \"./components\"\nimport {\n SearchIndex,\n setupClipboardJS,\n setupInstantLoading,\n setupVersionSelector\n} from \"./integrations\"\nimport {\n patchIndeterminate,\n patchScrollfix,\n patchScrolllock\n} from \"./patches\"\nimport \"./polyfills\"\n\n/* ----------------------------------------------------------------------------\n * Application\n * ------------------------------------------------------------------------- */\n\n/* Yay, JavaScript is available */\ndocument.documentElement.classList.remove(\"no-js\")\ndocument.documentElement.classList.add(\"js\")\n\n/* Set up navigation observables and subjects */\nconst document$ = watchDocument()\nconst location$ = watchLocation()\nconst target$ = watchLocationTarget()\nconst keyboard$ = watchKeyboard()\n\n/* Set up media observables */\nconst viewport$ = watchViewport()\nconst tablet$ = watchMedia(\"(min-width: 960px)\")\nconst screen$ = watchMedia(\"(min-width: 1220px)\")\nconst print$ = watchPrint()\n\n/* Retrieve search index, if search is enabled */\nconst config = configuration()\nconst index$ = document.forms.namedItem(\"search\")\n ? __search?.index || requestJSON(\n new URL(\"search/search_index.json\", config.base)\n )\n : NEVER\n\n/* Set up Clipboard.js integration */\nconst alert$ = new Subject()\nsetupClipboardJS({ alert$ })\n\n/* Set up instant loading, if enabled */\nif (feature(\"navigation.instant\"))\n setupInstantLoading({ document$, location$, viewport$ })\n\n/* Set up version selector */\nif (config.version?.provider === \"mike\")\n setupVersionSelector({ document$ })\n\n/* Always close drawer and search on navigation */\nmerge(location$, target$)\n .pipe(\n delay(125)\n )\n .subscribe(() => {\n setToggle(\"drawer\", false)\n setToggle(\"search\", false)\n })\n\n/* Set up global keyboard handlers */\nkeyboard$\n .pipe(\n filter(({ mode }) => mode === \"global\")\n )\n .subscribe(key => {\n switch (key.type) {\n\n /* Go to previous page */\n case \"p\":\n case \",\":\n const prev = getOptionalElement(\"[href][rel=prev]\")\n if (typeof prev !== \"undefined\")\n prev.click()\n break\n\n /* Go to next page */\n case \"n\":\n case \".\":\n const next = getOptionalElement(\"[href][rel=next]\")\n if (typeof next !== \"undefined\")\n next.click()\n break\n }\n })\n\n/* Set up patches */\npatchIndeterminate({ document$, tablet$ })\npatchScrollfix({ document$ })\npatchScrolllock({ viewport$, tablet$ })\n\n/* Set up header and main area observable */\nconst header$ = watchHeader(getComponentElement(\"header\"), { viewport$ })\nconst main$ = document$\n .pipe(\n map(() => getComponentElement(\"main\")),\n switchMap(el => watchMain(el, { viewport$, header$ })),\n shareReplay(1)\n )\n\n/* Set up control component observables */\nconst control$ = merge(\n\n /* Dialog */\n ...getComponentElements(\"dialog\")\n .map(el => mountDialog(el, { alert$ })),\n\n /* Header */\n ...getComponentElements(\"header\")\n .map(el => mountHeader(el, { viewport$, header$, main$ })),\n\n /* Color palette */\n ...getComponentElements(\"palette\")\n .map(el => mountPalette(el)),\n\n /* Search */\n ...getComponentElements(\"search\")\n .map(el => mountSearch(el, { index$, keyboard$ })),\n\n /* Repository information */\n ...getComponentElements(\"source\")\n .map(el => mountSource(el))\n)\n\n/* Set up content component observables */\nconst content$ = defer(() => merge(\n\n /* Content */\n ...getComponentElements(\"content\")\n .map(el => mountContent(el, { target$, print$ })),\n\n /* Search highlighting */\n ...getComponentElements(\"content\")\n .map(el => feature(\"search.highlight\")\n ? mountSearchHiglight(el, { index$, location$ })\n : EMPTY\n ),\n\n /* Header title */\n ...getComponentElements(\"header-title\")\n .map(el => mountHeaderTitle(el, { viewport$, header$ })),\n\n /* Sidebar */\n ...getComponentElements(\"sidebar\")\n .map(el => el.getAttribute(\"data-md-type\") === \"navigation\"\n ? at(screen$, () => mountSidebar(el, { viewport$, header$, main$ }))\n : at(tablet$, () => mountSidebar(el, { viewport$, header$, main$ }))\n ),\n\n /* Navigation tabs */\n ...getComponentElements(\"tabs\")\n .map(el => mountTabs(el, { viewport$, header$ })),\n\n /* Table of contents */\n ...getComponentElements(\"toc\")\n .map(el => mountTableOfContents(el, { viewport$, header$, target$ })),\n\n /* Back-to-top button */\n ...getComponentElements(\"top\")\n .map(el => mountBackToTop(el, { viewport$, header$, main$, target$ }))\n))\n\n/* Set up component observables */\nconst component$ = document$\n .pipe(\n switchMap(() => content$),\n mergeWith(control$),\n shareReplay(1)\n )\n\n/* Subscribe to all components */\ncomponent$.subscribe()\n\n/* ----------------------------------------------------------------------------\n * Exports\n * ------------------------------------------------------------------------- */\n\nwindow.document$ = document$ /* Document observable */\nwindow.location$ = location$ /* Location subject */\nwindow.target$ = target$ /* Location target observable */\nwindow.keyboard$ = keyboard$ /* Keyboard observable */\nwindow.viewport$ = viewport$ /* Viewport observable */\nwindow.tablet$ = tablet$ /* Media tablet observable */\nwindow.screen$ = screen$ /* Media screen observable */\nwindow.print$ = print$ /* Media print observable */\nwindow.alert$ = alert$ /* Alert subject */\nwindow.component$ = component$ /* Component observable */\n", "self.fetch||(self.fetch=function(e,n){return n=n||{},new Promise(function(t,s){var r=new XMLHttpRequest,o=[],u=[],i={},a=function(){return{ok:2==(r.status/100|0),statusText:r.statusText,status:r.status,url:r.responseURL,text:function(){return Promise.resolve(r.responseText)},json:function(){return Promise.resolve(r.responseText).then(JSON.parse)},blob:function(){return Promise.resolve(new Blob([r.response]))},clone:a,headers:{keys:function(){return o},entries:function(){return u},get:function(e){return i[e.toLowerCase()]},has:function(e){return e.toLowerCase()in i}}}};for(var c in r.open(n.method||\"get\",e,!0),r.onload=function(){r.getAllResponseHeaders().replace(/^(.*?):[^\\S\\n]*([\\s\\S]*?)$/gm,function(e,n,t){o.push(n=n.toLowerCase()),u.push([n,t]),i[n]=i[n]?i[n]+\",\"+t:t}),t(a())},r.onerror=s,r.withCredentials=\"include\"==n.credentials,n.headers)r.setRequestHeader(c,n.headers[c]);r.send(n.body||null)})});\n", "import tslib from '../tslib.js';\r\nconst {\r\n __extends,\r\n __assign,\r\n __rest,\r\n __decorate,\r\n __param,\r\n __metadata,\r\n __awaiter,\r\n __generator,\r\n __exportStar,\r\n __createBinding,\r\n __values,\r\n __read,\r\n __spread,\r\n __spreadArrays,\r\n __spreadArray,\r\n __await,\r\n __asyncGenerator,\r\n __asyncDelegator,\r\n __asyncValues,\r\n __makeTemplateObject,\r\n __importStar,\r\n __importDefault,\r\n __classPrivateFieldGet,\r\n __classPrivateFieldSet,\r\n} = tslib;\r\nexport {\r\n __extends,\r\n __assign,\r\n __rest,\r\n __decorate,\r\n __param,\r\n __metadata,\r\n __awaiter,\r\n __generator,\r\n __exportStar,\r\n __createBinding,\r\n __values,\r\n __read,\r\n __spread,\r\n __spreadArrays,\r\n __spreadArray,\r\n __await,\r\n __asyncGenerator,\r\n __asyncDelegator,\r\n __asyncValues,\r\n __makeTemplateObject,\r\n __importStar,\r\n __importDefault,\r\n __classPrivateFieldGet,\r\n __classPrivateFieldSet,\r\n};\r\n", null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n ReplaySubject,\n Subject,\n fromEvent\n} from \"rxjs\"\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch document\n *\n * Documents are implemented as subjects, so all downstream observables are\n * automatically updated when a new document is emitted.\n *\n * @returns Document subject\n */\nexport function watchDocument(): Subject {\n const document$ = new ReplaySubject(1)\n fromEvent(document, \"DOMContentLoaded\", { once: true })\n .subscribe(() => document$.next(document))\n\n /* Return document */\n return document$\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Retrieve all elements matching the query selector\n *\n * @template T - Element type\n *\n * @param selector - Query selector\n * @param node - Node of reference\n *\n * @returns Elements\n */\nexport function getElements(\n selector: T, node?: ParentNode\n): HTMLElementTagNameMap[T][]\n\nexport function getElements(\n selector: string, node?: ParentNode\n): T[]\n\nexport function getElements(\n selector: string, node: ParentNode = document\n): T[] {\n return Array.from(node.querySelectorAll(selector))\n}\n\n/**\n * Retrieve an element matching a query selector or throw a reference error\n *\n * Note that this function assumes that the element is present. If unsure if an\n * element is existent, use the `getOptionalElement` function instead.\n *\n * @template T - Element type\n *\n * @param selector - Query selector\n * @param node - Node of reference\n *\n * @returns Element\n */\nexport function getElement(\n selector: T, node?: ParentNode\n): HTMLElementTagNameMap[T]\n\nexport function getElement(\n selector: string, node?: ParentNode\n): T\n\nexport function getElement(\n selector: string, node: ParentNode = document\n): T {\n const el = getOptionalElement(selector, node)\n if (typeof el === \"undefined\")\n throw new ReferenceError(\n `Missing element: expected \"${selector}\" to be present`\n )\n\n /* Return element */\n return el\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Retrieve an optional element matching the query selector\n *\n * @template T - Element type\n *\n * @param selector - Query selector\n * @param node - Node of reference\n *\n * @returns Element or nothing\n */\nexport function getOptionalElement(\n selector: T, node?: ParentNode\n): HTMLElementTagNameMap[T] | undefined\n\nexport function getOptionalElement(\n selector: string, node?: ParentNode\n): T | undefined\n\nexport function getOptionalElement(\n selector: string, node: ParentNode = document\n): T | undefined {\n return node.querySelector(selector) || undefined\n}\n\n/**\n * Retrieve the currently active element\n *\n * @returns Element or nothing\n */\nexport function getActiveElement(): HTMLElement | undefined {\n return document.activeElement instanceof HTMLElement\n ? document.activeElement || undefined\n : undefined\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n Observable,\n debounceTime,\n distinctUntilChanged,\n fromEvent,\n map,\n merge,\n startWith\n} from \"rxjs\"\n\nimport { getActiveElement } from \"../_\"\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch element focus\n *\n * Previously, this function used `focus` and `blur` events to determine whether\n * an element is focused, but this doesn't work if there are focusable elements\n * within the elements itself. A better solutions are `focusin` and `focusout`\n * events, which bubble up the tree and allow for more fine-grained control.\n *\n * `debounceTime` is necessary, because when a focus change happens inside an\n * element, the observable would first emit `false` and then `true` again.\n *\n * @param el - Element\n *\n * @returns Element focus observable\n */\nexport function watchElementFocus(\n el: HTMLElement\n): Observable {\n return merge(\n fromEvent(document.body, \"focusin\"),\n fromEvent(document.body, \"focusout\")\n )\n .pipe(\n debounceTime(1),\n map(() => {\n const active = getActiveElement()\n return typeof active !== \"undefined\"\n ? el.contains(active)\n : false\n }),\n startWith(el === getActiveElement()),\n distinctUntilChanged()\n )\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n Observable,\n animationFrameScheduler,\n auditTime,\n fromEvent,\n map,\n merge,\n startWith\n} from \"rxjs\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Element offset\n */\nexport interface ElementOffset {\n x: number /* Horizontal offset */\n y: number /* Vertical offset */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Retrieve element offset\n *\n * @param el - Element\n *\n * @returns Element offset\n */\nexport function getElementOffset(\n el: HTMLElement\n): ElementOffset {\n return {\n x: el.offsetLeft,\n y: el.offsetTop\n }\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Watch element offset\n *\n * @param el - Element\n *\n * @returns Element offset observable\n */\nexport function watchElementOffset(\n el: HTMLElement\n): Observable {\n return merge(\n fromEvent(window, \"load\"),\n fromEvent(window, \"resize\")\n )\n .pipe(\n auditTime(0, animationFrameScheduler),\n map(() => getElementOffset(el)),\n startWith(getElementOffset(el))\n )\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n Observable,\n animationFrameScheduler,\n auditTime,\n fromEvent,\n map,\n merge,\n startWith\n} from \"rxjs\"\n\nimport { ElementOffset } from \"../_\"\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Retrieve element content offset (= scroll offset)\n *\n * @param el - Element\n *\n * @returns Element content offset\n */\nexport function getElementContentOffset(\n el: HTMLElement\n): ElementOffset {\n return {\n x: el.scrollLeft,\n y: el.scrollTop\n }\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Watch element content offset\n *\n * @param el - Element\n *\n * @returns Element content offset observable\n */\nexport function watchElementContentOffset(\n el: HTMLElement\n): Observable {\n return merge(\n fromEvent(el, \"scroll\"),\n fromEvent(window, \"resize\")\n )\n .pipe(\n auditTime(0, animationFrameScheduler),\n map(() => getElementContentOffset(el)),\n startWith(getElementContentOffset(el))\n )\n}\n", "/**\r\n * A collection of shims that provide minimal functionality of the ES6 collections.\r\n *\r\n * These implementations are not meant to be used outside of the ResizeObserver\r\n * modules as they cover only a limited range of use cases.\r\n */\r\n/* eslint-disable require-jsdoc, valid-jsdoc */\r\nvar MapShim = (function () {\r\n if (typeof Map !== 'undefined') {\r\n return Map;\r\n }\r\n /**\r\n * Returns index in provided array that matches the specified key.\r\n *\r\n * @param {Array} arr\r\n * @param {*} key\r\n * @returns {number}\r\n */\r\n function getIndex(arr, key) {\r\n var result = -1;\r\n arr.some(function (entry, index) {\r\n if (entry[0] === key) {\r\n result = index;\r\n return true;\r\n }\r\n return false;\r\n });\r\n return result;\r\n }\r\n return /** @class */ (function () {\r\n function class_1() {\r\n this.__entries__ = [];\r\n }\r\n Object.defineProperty(class_1.prototype, \"size\", {\r\n /**\r\n * @returns {boolean}\r\n */\r\n get: function () {\r\n return this.__entries__.length;\r\n },\r\n enumerable: true,\r\n configurable: true\r\n });\r\n /**\r\n * @param {*} key\r\n * @returns {*}\r\n */\r\n class_1.prototype.get = function (key) {\r\n var index = getIndex(this.__entries__, key);\r\n var entry = this.__entries__[index];\r\n return entry && entry[1];\r\n };\r\n /**\r\n * @param {*} key\r\n * @param {*} value\r\n * @returns {void}\r\n */\r\n class_1.prototype.set = function (key, value) {\r\n var index = getIndex(this.__entries__, key);\r\n if (~index) {\r\n this.__entries__[index][1] = value;\r\n }\r\n else {\r\n this.__entries__.push([key, value]);\r\n }\r\n };\r\n /**\r\n * @param {*} key\r\n * @returns {void}\r\n */\r\n class_1.prototype.delete = function (key) {\r\n var entries = this.__entries__;\r\n var index = getIndex(entries, key);\r\n if (~index) {\r\n entries.splice(index, 1);\r\n }\r\n };\r\n /**\r\n * @param {*} key\r\n * @returns {void}\r\n */\r\n class_1.prototype.has = function (key) {\r\n return !!~getIndex(this.__entries__, key);\r\n };\r\n /**\r\n * @returns {void}\r\n */\r\n class_1.prototype.clear = function () {\r\n this.__entries__.splice(0);\r\n };\r\n /**\r\n * @param {Function} callback\r\n * @param {*} [ctx=null]\r\n * @returns {void}\r\n */\r\n class_1.prototype.forEach = function (callback, ctx) {\r\n if (ctx === void 0) { ctx = null; }\r\n for (var _i = 0, _a = this.__entries__; _i < _a.length; _i++) {\r\n var entry = _a[_i];\r\n callback.call(ctx, entry[1], entry[0]);\r\n }\r\n };\r\n return class_1;\r\n }());\r\n})();\n\n/**\r\n * Detects whether window and document objects are available in current environment.\r\n */\r\nvar isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined' && window.document === document;\n\n// Returns global object of a current environment.\r\nvar global$1 = (function () {\r\n if (typeof global !== 'undefined' && global.Math === Math) {\r\n return global;\r\n }\r\n if (typeof self !== 'undefined' && self.Math === Math) {\r\n return self;\r\n }\r\n if (typeof window !== 'undefined' && window.Math === Math) {\r\n return window;\r\n }\r\n // eslint-disable-next-line no-new-func\r\n return Function('return this')();\r\n})();\n\n/**\r\n * A shim for the requestAnimationFrame which falls back to the setTimeout if\r\n * first one is not supported.\r\n *\r\n * @returns {number} Requests' identifier.\r\n */\r\nvar requestAnimationFrame$1 = (function () {\r\n if (typeof requestAnimationFrame === 'function') {\r\n // It's required to use a bounded function because IE sometimes throws\r\n // an \"Invalid calling object\" error if rAF is invoked without the global\r\n // object on the left hand side.\r\n return requestAnimationFrame.bind(global$1);\r\n }\r\n return function (callback) { return setTimeout(function () { return callback(Date.now()); }, 1000 / 60); };\r\n})();\n\n// Defines minimum timeout before adding a trailing call.\r\nvar trailingTimeout = 2;\r\n/**\r\n * Creates a wrapper function which ensures that provided callback will be\r\n * invoked only once during the specified delay period.\r\n *\r\n * @param {Function} callback - Function to be invoked after the delay period.\r\n * @param {number} delay - Delay after which to invoke callback.\r\n * @returns {Function}\r\n */\r\nfunction throttle (callback, delay) {\r\n var leadingCall = false, trailingCall = false, lastCallTime = 0;\r\n /**\r\n * Invokes the original callback function and schedules new invocation if\r\n * the \"proxy\" was called during current request.\r\n *\r\n * @returns {void}\r\n */\r\n function resolvePending() {\r\n if (leadingCall) {\r\n leadingCall = false;\r\n callback();\r\n }\r\n if (trailingCall) {\r\n proxy();\r\n }\r\n }\r\n /**\r\n * Callback invoked after the specified delay. It will further postpone\r\n * invocation of the original function delegating it to the\r\n * requestAnimationFrame.\r\n *\r\n * @returns {void}\r\n */\r\n function timeoutCallback() {\r\n requestAnimationFrame$1(resolvePending);\r\n }\r\n /**\r\n * Schedules invocation of the original function.\r\n *\r\n * @returns {void}\r\n */\r\n function proxy() {\r\n var timeStamp = Date.now();\r\n if (leadingCall) {\r\n // Reject immediately following calls.\r\n if (timeStamp - lastCallTime < trailingTimeout) {\r\n return;\r\n }\r\n // Schedule new call to be in invoked when the pending one is resolved.\r\n // This is important for \"transitions\" which never actually start\r\n // immediately so there is a chance that we might miss one if change\r\n // happens amids the pending invocation.\r\n trailingCall = true;\r\n }\r\n else {\r\n leadingCall = true;\r\n trailingCall = false;\r\n setTimeout(timeoutCallback, delay);\r\n }\r\n lastCallTime = timeStamp;\r\n }\r\n return proxy;\r\n}\n\n// Minimum delay before invoking the update of observers.\r\nvar REFRESH_DELAY = 20;\r\n// A list of substrings of CSS properties used to find transition events that\r\n// might affect dimensions of observed elements.\r\nvar transitionKeys = ['top', 'right', 'bottom', 'left', 'width', 'height', 'size', 'weight'];\r\n// Check if MutationObserver is available.\r\nvar mutationObserverSupported = typeof MutationObserver !== 'undefined';\r\n/**\r\n * Singleton controller class which handles updates of ResizeObserver instances.\r\n */\r\nvar ResizeObserverController = /** @class */ (function () {\r\n /**\r\n * Creates a new instance of ResizeObserverController.\r\n *\r\n * @private\r\n */\r\n function ResizeObserverController() {\r\n /**\r\n * Indicates whether DOM listeners have been added.\r\n *\r\n * @private {boolean}\r\n */\r\n this.connected_ = false;\r\n /**\r\n * Tells that controller has subscribed for Mutation Events.\r\n *\r\n * @private {boolean}\r\n */\r\n this.mutationEventsAdded_ = false;\r\n /**\r\n * Keeps reference to the instance of MutationObserver.\r\n *\r\n * @private {MutationObserver}\r\n */\r\n this.mutationsObserver_ = null;\r\n /**\r\n * A list of connected observers.\r\n *\r\n * @private {Array}\r\n */\r\n this.observers_ = [];\r\n this.onTransitionEnd_ = this.onTransitionEnd_.bind(this);\r\n this.refresh = throttle(this.refresh.bind(this), REFRESH_DELAY);\r\n }\r\n /**\r\n * Adds observer to observers list.\r\n *\r\n * @param {ResizeObserverSPI} observer - Observer to be added.\r\n * @returns {void}\r\n */\r\n ResizeObserverController.prototype.addObserver = function (observer) {\r\n if (!~this.observers_.indexOf(observer)) {\r\n this.observers_.push(observer);\r\n }\r\n // Add listeners if they haven't been added yet.\r\n if (!this.connected_) {\r\n this.connect_();\r\n }\r\n };\r\n /**\r\n * Removes observer from observers list.\r\n *\r\n * @param {ResizeObserverSPI} observer - Observer to be removed.\r\n * @returns {void}\r\n */\r\n ResizeObserverController.prototype.removeObserver = function (observer) {\r\n var observers = this.observers_;\r\n var index = observers.indexOf(observer);\r\n // Remove observer if it's present in registry.\r\n if (~index) {\r\n observers.splice(index, 1);\r\n }\r\n // Remove listeners if controller has no connected observers.\r\n if (!observers.length && this.connected_) {\r\n this.disconnect_();\r\n }\r\n };\r\n /**\r\n * Invokes the update of observers. It will continue running updates insofar\r\n * it detects changes.\r\n *\r\n * @returns {void}\r\n */\r\n ResizeObserverController.prototype.refresh = function () {\r\n var changesDetected = this.updateObservers_();\r\n // Continue running updates if changes have been detected as there might\r\n // be future ones caused by CSS transitions.\r\n if (changesDetected) {\r\n this.refresh();\r\n }\r\n };\r\n /**\r\n * Updates every observer from observers list and notifies them of queued\r\n * entries.\r\n *\r\n * @private\r\n * @returns {boolean} Returns \"true\" if any observer has detected changes in\r\n * dimensions of it's elements.\r\n */\r\n ResizeObserverController.prototype.updateObservers_ = function () {\r\n // Collect observers that have active observations.\r\n var activeObservers = this.observers_.filter(function (observer) {\r\n return observer.gatherActive(), observer.hasActive();\r\n });\r\n // Deliver notifications in a separate cycle in order to avoid any\r\n // collisions between observers, e.g. when multiple instances of\r\n // ResizeObserver are tracking the same element and the callback of one\r\n // of them changes content dimensions of the observed target. Sometimes\r\n // this may result in notifications being blocked for the rest of observers.\r\n activeObservers.forEach(function (observer) { return observer.broadcastActive(); });\r\n return activeObservers.length > 0;\r\n };\r\n /**\r\n * Initializes DOM listeners.\r\n *\r\n * @private\r\n * @returns {void}\r\n */\r\n ResizeObserverController.prototype.connect_ = function () {\r\n // Do nothing if running in a non-browser environment or if listeners\r\n // have been already added.\r\n if (!isBrowser || this.connected_) {\r\n return;\r\n }\r\n // Subscription to the \"Transitionend\" event is used as a workaround for\r\n // delayed transitions. This way it's possible to capture at least the\r\n // final state of an element.\r\n document.addEventListener('transitionend', this.onTransitionEnd_);\r\n window.addEventListener('resize', this.refresh);\r\n if (mutationObserverSupported) {\r\n this.mutationsObserver_ = new MutationObserver(this.refresh);\r\n this.mutationsObserver_.observe(document, {\r\n attributes: true,\r\n childList: true,\r\n characterData: true,\r\n subtree: true\r\n });\r\n }\r\n else {\r\n document.addEventListener('DOMSubtreeModified', this.refresh);\r\n this.mutationEventsAdded_ = true;\r\n }\r\n this.connected_ = true;\r\n };\r\n /**\r\n * Removes DOM listeners.\r\n *\r\n * @private\r\n * @returns {void}\r\n */\r\n ResizeObserverController.prototype.disconnect_ = function () {\r\n // Do nothing if running in a non-browser environment or if listeners\r\n // have been already removed.\r\n if (!isBrowser || !this.connected_) {\r\n return;\r\n }\r\n document.removeEventListener('transitionend', this.onTransitionEnd_);\r\n window.removeEventListener('resize', this.refresh);\r\n if (this.mutationsObserver_) {\r\n this.mutationsObserver_.disconnect();\r\n }\r\n if (this.mutationEventsAdded_) {\r\n document.removeEventListener('DOMSubtreeModified', this.refresh);\r\n }\r\n this.mutationsObserver_ = null;\r\n this.mutationEventsAdded_ = false;\r\n this.connected_ = false;\r\n };\r\n /**\r\n * \"Transitionend\" event handler.\r\n *\r\n * @private\r\n * @param {TransitionEvent} event\r\n * @returns {void}\r\n */\r\n ResizeObserverController.prototype.onTransitionEnd_ = function (_a) {\r\n var _b = _a.propertyName, propertyName = _b === void 0 ? '' : _b;\r\n // Detect whether transition may affect dimensions of an element.\r\n var isReflowProperty = transitionKeys.some(function (key) {\r\n return !!~propertyName.indexOf(key);\r\n });\r\n if (isReflowProperty) {\r\n this.refresh();\r\n }\r\n };\r\n /**\r\n * Returns instance of the ResizeObserverController.\r\n *\r\n * @returns {ResizeObserverController}\r\n */\r\n ResizeObserverController.getInstance = function () {\r\n if (!this.instance_) {\r\n this.instance_ = new ResizeObserverController();\r\n }\r\n return this.instance_;\r\n };\r\n /**\r\n * Holds reference to the controller's instance.\r\n *\r\n * @private {ResizeObserverController}\r\n */\r\n ResizeObserverController.instance_ = null;\r\n return ResizeObserverController;\r\n}());\n\n/**\r\n * Defines non-writable/enumerable properties of the provided target object.\r\n *\r\n * @param {Object} target - Object for which to define properties.\r\n * @param {Object} props - Properties to be defined.\r\n * @returns {Object} Target object.\r\n */\r\nvar defineConfigurable = (function (target, props) {\r\n for (var _i = 0, _a = Object.keys(props); _i < _a.length; _i++) {\r\n var key = _a[_i];\r\n Object.defineProperty(target, key, {\r\n value: props[key],\r\n enumerable: false,\r\n writable: false,\r\n configurable: true\r\n });\r\n }\r\n return target;\r\n});\n\n/**\r\n * Returns the global object associated with provided element.\r\n *\r\n * @param {Object} target\r\n * @returns {Object}\r\n */\r\nvar getWindowOf = (function (target) {\r\n // Assume that the element is an instance of Node, which means that it\r\n // has the \"ownerDocument\" property from which we can retrieve a\r\n // corresponding global object.\r\n var ownerGlobal = target && target.ownerDocument && target.ownerDocument.defaultView;\r\n // Return the local global object if it's not possible extract one from\r\n // provided element.\r\n return ownerGlobal || global$1;\r\n});\n\n// Placeholder of an empty content rectangle.\r\nvar emptyRect = createRectInit(0, 0, 0, 0);\r\n/**\r\n * Converts provided string to a number.\r\n *\r\n * @param {number|string} value\r\n * @returns {number}\r\n */\r\nfunction toFloat(value) {\r\n return parseFloat(value) || 0;\r\n}\r\n/**\r\n * Extracts borders size from provided styles.\r\n *\r\n * @param {CSSStyleDeclaration} styles\r\n * @param {...string} positions - Borders positions (top, right, ...)\r\n * @returns {number}\r\n */\r\nfunction getBordersSize(styles) {\r\n var positions = [];\r\n for (var _i = 1; _i < arguments.length; _i++) {\r\n positions[_i - 1] = arguments[_i];\r\n }\r\n return positions.reduce(function (size, position) {\r\n var value = styles['border-' + position + '-width'];\r\n return size + toFloat(value);\r\n }, 0);\r\n}\r\n/**\r\n * Extracts paddings sizes from provided styles.\r\n *\r\n * @param {CSSStyleDeclaration} styles\r\n * @returns {Object} Paddings box.\r\n */\r\nfunction getPaddings(styles) {\r\n var positions = ['top', 'right', 'bottom', 'left'];\r\n var paddings = {};\r\n for (var _i = 0, positions_1 = positions; _i < positions_1.length; _i++) {\r\n var position = positions_1[_i];\r\n var value = styles['padding-' + position];\r\n paddings[position] = toFloat(value);\r\n }\r\n return paddings;\r\n}\r\n/**\r\n * Calculates content rectangle of provided SVG element.\r\n *\r\n * @param {SVGGraphicsElement} target - Element content rectangle of which needs\r\n * to be calculated.\r\n * @returns {DOMRectInit}\r\n */\r\nfunction getSVGContentRect(target) {\r\n var bbox = target.getBBox();\r\n return createRectInit(0, 0, bbox.width, bbox.height);\r\n}\r\n/**\r\n * Calculates content rectangle of provided HTMLElement.\r\n *\r\n * @param {HTMLElement} target - Element for which to calculate the content rectangle.\r\n * @returns {DOMRectInit}\r\n */\r\nfunction getHTMLElementContentRect(target) {\r\n // Client width & height properties can't be\r\n // used exclusively as they provide rounded values.\r\n var clientWidth = target.clientWidth, clientHeight = target.clientHeight;\r\n // By this condition we can catch all non-replaced inline, hidden and\r\n // detached elements. Though elements with width & height properties less\r\n // than 0.5 will be discarded as well.\r\n //\r\n // Without it we would need to implement separate methods for each of\r\n // those cases and it's not possible to perform a precise and performance\r\n // effective test for hidden elements. E.g. even jQuery's ':visible' filter\r\n // gives wrong results for elements with width & height less than 0.5.\r\n if (!clientWidth && !clientHeight) {\r\n return emptyRect;\r\n }\r\n var styles = getWindowOf(target).getComputedStyle(target);\r\n var paddings = getPaddings(styles);\r\n var horizPad = paddings.left + paddings.right;\r\n var vertPad = paddings.top + paddings.bottom;\r\n // Computed styles of width & height are being used because they are the\r\n // only dimensions available to JS that contain non-rounded values. It could\r\n // be possible to utilize the getBoundingClientRect if only it's data wasn't\r\n // affected by CSS transformations let alone paddings, borders and scroll bars.\r\n var width = toFloat(styles.width), height = toFloat(styles.height);\r\n // Width & height include paddings and borders when the 'border-box' box\r\n // model is applied (except for IE).\r\n if (styles.boxSizing === 'border-box') {\r\n // Following conditions are required to handle Internet Explorer which\r\n // doesn't include paddings and borders to computed CSS dimensions.\r\n //\r\n // We can say that if CSS dimensions + paddings are equal to the \"client\"\r\n // properties then it's either IE, and thus we don't need to subtract\r\n // anything, or an element merely doesn't have paddings/borders styles.\r\n if (Math.round(width + horizPad) !== clientWidth) {\r\n width -= getBordersSize(styles, 'left', 'right') + horizPad;\r\n }\r\n if (Math.round(height + vertPad) !== clientHeight) {\r\n height -= getBordersSize(styles, 'top', 'bottom') + vertPad;\r\n }\r\n }\r\n // Following steps can't be applied to the document's root element as its\r\n // client[Width/Height] properties represent viewport area of the window.\r\n // Besides, it's as well not necessary as the itself neither has\r\n // rendered scroll bars nor it can be clipped.\r\n if (!isDocumentElement(target)) {\r\n // In some browsers (only in Firefox, actually) CSS width & height\r\n // include scroll bars size which can be removed at this step as scroll\r\n // bars are the only difference between rounded dimensions + paddings\r\n // and \"client\" properties, though that is not always true in Chrome.\r\n var vertScrollbar = Math.round(width + horizPad) - clientWidth;\r\n var horizScrollbar = Math.round(height + vertPad) - clientHeight;\r\n // Chrome has a rather weird rounding of \"client\" properties.\r\n // E.g. for an element with content width of 314.2px it sometimes gives\r\n // the client width of 315px and for the width of 314.7px it may give\r\n // 314px. And it doesn't happen all the time. So just ignore this delta\r\n // as a non-relevant.\r\n if (Math.abs(vertScrollbar) !== 1) {\r\n width -= vertScrollbar;\r\n }\r\n if (Math.abs(horizScrollbar) !== 1) {\r\n height -= horizScrollbar;\r\n }\r\n }\r\n return createRectInit(paddings.left, paddings.top, width, height);\r\n}\r\n/**\r\n * Checks whether provided element is an instance of the SVGGraphicsElement.\r\n *\r\n * @param {Element} target - Element to be checked.\r\n * @returns {boolean}\r\n */\r\nvar isSVGGraphicsElement = (function () {\r\n // Some browsers, namely IE and Edge, don't have the SVGGraphicsElement\r\n // interface.\r\n if (typeof SVGGraphicsElement !== 'undefined') {\r\n return function (target) { return target instanceof getWindowOf(target).SVGGraphicsElement; };\r\n }\r\n // If it's so, then check that element is at least an instance of the\r\n // SVGElement and that it has the \"getBBox\" method.\r\n // eslint-disable-next-line no-extra-parens\r\n return function (target) { return (target instanceof getWindowOf(target).SVGElement &&\r\n typeof target.getBBox === 'function'); };\r\n})();\r\n/**\r\n * Checks whether provided element is a document element ().\r\n *\r\n * @param {Element} target - Element to be checked.\r\n * @returns {boolean}\r\n */\r\nfunction isDocumentElement(target) {\r\n return target === getWindowOf(target).document.documentElement;\r\n}\r\n/**\r\n * Calculates an appropriate content rectangle for provided html or svg element.\r\n *\r\n * @param {Element} target - Element content rectangle of which needs to be calculated.\r\n * @returns {DOMRectInit}\r\n */\r\nfunction getContentRect(target) {\r\n if (!isBrowser) {\r\n return emptyRect;\r\n }\r\n if (isSVGGraphicsElement(target)) {\r\n return getSVGContentRect(target);\r\n }\r\n return getHTMLElementContentRect(target);\r\n}\r\n/**\r\n * Creates rectangle with an interface of the DOMRectReadOnly.\r\n * Spec: https://drafts.fxtf.org/geometry/#domrectreadonly\r\n *\r\n * @param {DOMRectInit} rectInit - Object with rectangle's x/y coordinates and dimensions.\r\n * @returns {DOMRectReadOnly}\r\n */\r\nfunction createReadOnlyRect(_a) {\r\n var x = _a.x, y = _a.y, width = _a.width, height = _a.height;\r\n // If DOMRectReadOnly is available use it as a prototype for the rectangle.\r\n var Constr = typeof DOMRectReadOnly !== 'undefined' ? DOMRectReadOnly : Object;\r\n var rect = Object.create(Constr.prototype);\r\n // Rectangle's properties are not writable and non-enumerable.\r\n defineConfigurable(rect, {\r\n x: x, y: y, width: width, height: height,\r\n top: y,\r\n right: x + width,\r\n bottom: height + y,\r\n left: x\r\n });\r\n return rect;\r\n}\r\n/**\r\n * Creates DOMRectInit object based on the provided dimensions and the x/y coordinates.\r\n * Spec: https://drafts.fxtf.org/geometry/#dictdef-domrectinit\r\n *\r\n * @param {number} x - X coordinate.\r\n * @param {number} y - Y coordinate.\r\n * @param {number} width - Rectangle's width.\r\n * @param {number} height - Rectangle's height.\r\n * @returns {DOMRectInit}\r\n */\r\nfunction createRectInit(x, y, width, height) {\r\n return { x: x, y: y, width: width, height: height };\r\n}\n\n/**\r\n * Class that is responsible for computations of the content rectangle of\r\n * provided DOM element and for keeping track of it's changes.\r\n */\r\nvar ResizeObservation = /** @class */ (function () {\r\n /**\r\n * Creates an instance of ResizeObservation.\r\n *\r\n * @param {Element} target - Element to be observed.\r\n */\r\n function ResizeObservation(target) {\r\n /**\r\n * Broadcasted width of content rectangle.\r\n *\r\n * @type {number}\r\n */\r\n this.broadcastWidth = 0;\r\n /**\r\n * Broadcasted height of content rectangle.\r\n *\r\n * @type {number}\r\n */\r\n this.broadcastHeight = 0;\r\n /**\r\n * Reference to the last observed content rectangle.\r\n *\r\n * @private {DOMRectInit}\r\n */\r\n this.contentRect_ = createRectInit(0, 0, 0, 0);\r\n this.target = target;\r\n }\r\n /**\r\n * Updates content rectangle and tells whether it's width or height properties\r\n * have changed since the last broadcast.\r\n *\r\n * @returns {boolean}\r\n */\r\n ResizeObservation.prototype.isActive = function () {\r\n var rect = getContentRect(this.target);\r\n this.contentRect_ = rect;\r\n return (rect.width !== this.broadcastWidth ||\r\n rect.height !== this.broadcastHeight);\r\n };\r\n /**\r\n * Updates 'broadcastWidth' and 'broadcastHeight' properties with a data\r\n * from the corresponding properties of the last observed content rectangle.\r\n *\r\n * @returns {DOMRectInit} Last observed content rectangle.\r\n */\r\n ResizeObservation.prototype.broadcastRect = function () {\r\n var rect = this.contentRect_;\r\n this.broadcastWidth = rect.width;\r\n this.broadcastHeight = rect.height;\r\n return rect;\r\n };\r\n return ResizeObservation;\r\n}());\n\nvar ResizeObserverEntry = /** @class */ (function () {\r\n /**\r\n * Creates an instance of ResizeObserverEntry.\r\n *\r\n * @param {Element} target - Element that is being observed.\r\n * @param {DOMRectInit} rectInit - Data of the element's content rectangle.\r\n */\r\n function ResizeObserverEntry(target, rectInit) {\r\n var contentRect = createReadOnlyRect(rectInit);\r\n // According to the specification following properties are not writable\r\n // and are also not enumerable in the native implementation.\r\n //\r\n // Property accessors are not being used as they'd require to define a\r\n // private WeakMap storage which may cause memory leaks in browsers that\r\n // don't support this type of collections.\r\n defineConfigurable(this, { target: target, contentRect: contentRect });\r\n }\r\n return ResizeObserverEntry;\r\n}());\n\nvar ResizeObserverSPI = /** @class */ (function () {\r\n /**\r\n * Creates a new instance of ResizeObserver.\r\n *\r\n * @param {ResizeObserverCallback} callback - Callback function that is invoked\r\n * when one of the observed elements changes it's content dimensions.\r\n * @param {ResizeObserverController} controller - Controller instance which\r\n * is responsible for the updates of observer.\r\n * @param {ResizeObserver} callbackCtx - Reference to the public\r\n * ResizeObserver instance which will be passed to callback function.\r\n */\r\n function ResizeObserverSPI(callback, controller, callbackCtx) {\r\n /**\r\n * Collection of resize observations that have detected changes in dimensions\r\n * of elements.\r\n *\r\n * @private {Array}\r\n */\r\n this.activeObservations_ = [];\r\n /**\r\n * Registry of the ResizeObservation instances.\r\n *\r\n * @private {Map}\r\n */\r\n this.observations_ = new MapShim();\r\n if (typeof callback !== 'function') {\r\n throw new TypeError('The callback provided as parameter 1 is not a function.');\r\n }\r\n this.callback_ = callback;\r\n this.controller_ = controller;\r\n this.callbackCtx_ = callbackCtx;\r\n }\r\n /**\r\n * Starts observing provided element.\r\n *\r\n * @param {Element} target - Element to be observed.\r\n * @returns {void}\r\n */\r\n ResizeObserverSPI.prototype.observe = function (target) {\r\n if (!arguments.length) {\r\n throw new TypeError('1 argument required, but only 0 present.');\r\n }\r\n // Do nothing if current environment doesn't have the Element interface.\r\n if (typeof Element === 'undefined' || !(Element instanceof Object)) {\r\n return;\r\n }\r\n if (!(target instanceof getWindowOf(target).Element)) {\r\n throw new TypeError('parameter 1 is not of type \"Element\".');\r\n }\r\n var observations = this.observations_;\r\n // Do nothing if element is already being observed.\r\n if (observations.has(target)) {\r\n return;\r\n }\r\n observations.set(target, new ResizeObservation(target));\r\n this.controller_.addObserver(this);\r\n // Force the update of observations.\r\n this.controller_.refresh();\r\n };\r\n /**\r\n * Stops observing provided element.\r\n *\r\n * @param {Element} target - Element to stop observing.\r\n * @returns {void}\r\n */\r\n ResizeObserverSPI.prototype.unobserve = function (target) {\r\n if (!arguments.length) {\r\n throw new TypeError('1 argument required, but only 0 present.');\r\n }\r\n // Do nothing if current environment doesn't have the Element interface.\r\n if (typeof Element === 'undefined' || !(Element instanceof Object)) {\r\n return;\r\n }\r\n if (!(target instanceof getWindowOf(target).Element)) {\r\n throw new TypeError('parameter 1 is not of type \"Element\".');\r\n }\r\n var observations = this.observations_;\r\n // Do nothing if element is not being observed.\r\n if (!observations.has(target)) {\r\n return;\r\n }\r\n observations.delete(target);\r\n if (!observations.size) {\r\n this.controller_.removeObserver(this);\r\n }\r\n };\r\n /**\r\n * Stops observing all elements.\r\n *\r\n * @returns {void}\r\n */\r\n ResizeObserverSPI.prototype.disconnect = function () {\r\n this.clearActive();\r\n this.observations_.clear();\r\n this.controller_.removeObserver(this);\r\n };\r\n /**\r\n * Collects observation instances the associated element of which has changed\r\n * it's content rectangle.\r\n *\r\n * @returns {void}\r\n */\r\n ResizeObserverSPI.prototype.gatherActive = function () {\r\n var _this = this;\r\n this.clearActive();\r\n this.observations_.forEach(function (observation) {\r\n if (observation.isActive()) {\r\n _this.activeObservations_.push(observation);\r\n }\r\n });\r\n };\r\n /**\r\n * Invokes initial callback function with a list of ResizeObserverEntry\r\n * instances collected from active resize observations.\r\n *\r\n * @returns {void}\r\n */\r\n ResizeObserverSPI.prototype.broadcastActive = function () {\r\n // Do nothing if observer doesn't have active observations.\r\n if (!this.hasActive()) {\r\n return;\r\n }\r\n var ctx = this.callbackCtx_;\r\n // Create ResizeObserverEntry instance for every active observation.\r\n var entries = this.activeObservations_.map(function (observation) {\r\n return new ResizeObserverEntry(observation.target, observation.broadcastRect());\r\n });\r\n this.callback_.call(ctx, entries, ctx);\r\n this.clearActive();\r\n };\r\n /**\r\n * Clears the collection of active observations.\r\n *\r\n * @returns {void}\r\n */\r\n ResizeObserverSPI.prototype.clearActive = function () {\r\n this.activeObservations_.splice(0);\r\n };\r\n /**\r\n * Tells whether observer has active observations.\r\n *\r\n * @returns {boolean}\r\n */\r\n ResizeObserverSPI.prototype.hasActive = function () {\r\n return this.activeObservations_.length > 0;\r\n };\r\n return ResizeObserverSPI;\r\n}());\n\n// Registry of internal observers. If WeakMap is not available use current shim\r\n// for the Map collection as it has all required methods and because WeakMap\r\n// can't be fully polyfilled anyway.\r\nvar observers = typeof WeakMap !== 'undefined' ? new WeakMap() : new MapShim();\r\n/**\r\n * ResizeObserver API. Encapsulates the ResizeObserver SPI implementation\r\n * exposing only those methods and properties that are defined in the spec.\r\n */\r\nvar ResizeObserver = /** @class */ (function () {\r\n /**\r\n * Creates a new instance of ResizeObserver.\r\n *\r\n * @param {ResizeObserverCallback} callback - Callback that is invoked when\r\n * dimensions of the observed elements change.\r\n */\r\n function ResizeObserver(callback) {\r\n if (!(this instanceof ResizeObserver)) {\r\n throw new TypeError('Cannot call a class as a function.');\r\n }\r\n if (!arguments.length) {\r\n throw new TypeError('1 argument required, but only 0 present.');\r\n }\r\n var controller = ResizeObserverController.getInstance();\r\n var observer = new ResizeObserverSPI(callback, controller, this);\r\n observers.set(this, observer);\r\n }\r\n return ResizeObserver;\r\n}());\r\n// Expose public methods of ResizeObserver.\r\n[\r\n 'observe',\r\n 'unobserve',\r\n 'disconnect'\r\n].forEach(function (method) {\r\n ResizeObserver.prototype[method] = function () {\r\n var _a;\r\n return (_a = observers.get(this))[method].apply(_a, arguments);\r\n };\r\n});\n\nvar index = (function () {\r\n // Export existing implementation if available.\r\n if (typeof global$1.ResizeObserver !== 'undefined') {\r\n return global$1.ResizeObserver;\r\n }\r\n return ResizeObserver;\r\n})();\n\nexport default index;\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport ResizeObserver from \"resize-observer-polyfill\"\nimport {\n NEVER,\n Observable,\n Subject,\n defer,\n filter,\n finalize,\n map,\n merge,\n of,\n shareReplay,\n startWith,\n switchMap,\n tap\n} from \"rxjs\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Element offset\n */\nexport interface ElementSize {\n width: number /* Element width */\n height: number /* Element height */\n}\n\n/* ----------------------------------------------------------------------------\n * Data\n * ------------------------------------------------------------------------- */\n\n/**\n * Resize observer entry subject\n */\nconst entry$ = new Subject()\n\n/**\n * Resize observer observable\n *\n * This observable will create a `ResizeObserver` on the first subscription\n * and will automatically terminate it when there are no more subscribers.\n * It's quite important to centralize observation in a single `ResizeObserver`,\n * as the performance difference can be quite dramatic, as the link shows.\n *\n * @see https://bit.ly/3iIYfEm - Google Groups on performance\n */\nconst observer$ = defer(() => of(\n new ResizeObserver(entries => {\n for (const entry of entries)\n entry$.next(entry)\n })\n))\n .pipe(\n switchMap(observer => merge(NEVER, of(observer))\n .pipe(\n finalize(() => observer.disconnect())\n )\n ),\n shareReplay(1)\n )\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Retrieve element size\n *\n * @param el - Element\n *\n * @returns Element size\n */\nexport function getElementSize(\n el: HTMLElement\n): ElementSize {\n return {\n width: el.offsetWidth,\n height: el.offsetHeight\n }\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Watch element size\n *\n * This function returns an observable that subscribes to a single internal\n * instance of `ResizeObserver` upon subscription, and emit resize events until\n * termination. Note that this function should not be called with the same\n * element twice, as the first unsubscription will terminate observation.\n *\n * Sadly, we can't use the `DOMRect` objects returned by the observer, because\n * we need the emitted values to be consistent with `getElementSize`, which will\n * return the used values (rounded) and not actual values (unrounded). Thus, we\n * use the `offset*` properties. See the linked GitHub issue.\n *\n * @see https://bit.ly/3m0k3he - GitHub issue\n *\n * @param el - Element\n *\n * @returns Element size observable\n */\nexport function watchElementSize(\n el: HTMLElement\n): Observable {\n return observer$\n .pipe(\n tap(observer => observer.observe(el)),\n switchMap(observer => entry$\n .pipe(\n filter(({ target }) => target === el),\n finalize(() => observer.unobserve(el)),\n map(() => getElementSize(el))\n )\n ),\n startWith(getElementSize(el))\n )\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { ElementSize } from \"../_\"\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Retrieve element content size (= scroll width and height)\n *\n * @param el - Element\n *\n * @returns Element content size\n */\nexport function getElementContentSize(\n el: HTMLElement\n): ElementSize {\n return {\n width: el.scrollWidth,\n height: el.scrollHeight\n }\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n NEVER,\n Observable,\n Subject,\n defer,\n distinctUntilChanged,\n filter,\n finalize,\n map,\n merge,\n of,\n shareReplay,\n switchMap,\n tap\n} from \"rxjs\"\n\nimport {\n getElementContentSize,\n getElementSize,\n watchElementContentOffset\n} from \"~/browser\"\n\n/* ----------------------------------------------------------------------------\n * Data\n * ------------------------------------------------------------------------- */\n\n/**\n * Intersection observer entry subject\n */\nconst entry$ = new Subject()\n\n/**\n * Intersection observer observable\n *\n * This observable will create an `IntersectionObserver` on first subscription\n * and will automatically terminate it when there are no more subscribers.\n *\n * @see https://bit.ly/3iIYfEm - Google Groups on performance\n */\nconst observer$ = defer(() => of(\n new IntersectionObserver(entries => {\n for (const entry of entries)\n entry$.next(entry)\n }, {\n threshold: 0\n })\n))\n .pipe(\n switchMap(observer => merge(NEVER, of(observer))\n .pipe(\n finalize(() => observer.disconnect())\n )\n ),\n shareReplay(1)\n )\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch element visibility\n *\n * @param el - Element\n *\n * @returns Element visibility observable\n */\nexport function watchElementVisibility(\n el: HTMLElement\n): Observable {\n return observer$\n .pipe(\n tap(observer => observer.observe(el)),\n switchMap(observer => entry$\n .pipe(\n filter(({ target }) => target === el),\n finalize(() => observer.unobserve(el)),\n map(({ isIntersecting }) => isIntersecting)\n )\n )\n )\n}\n\n/**\n * Watch element boundary\n *\n * This function returns an observable which emits whether the bottom content\n * boundary (= scroll offset) of an element is within a certain threshold.\n *\n * @param el - Element\n * @param threshold - Threshold\n *\n * @returns Element boundary observable\n */\nexport function watchElementBoundary(\n el: HTMLElement, threshold = 16\n): Observable {\n return watchElementContentOffset(el)\n .pipe(\n map(({ y }) => {\n const visible = getElementSize(el)\n const content = getElementContentSize(el)\n return y >= (\n content.height - visible.height - threshold\n )\n }),\n distinctUntilChanged()\n )\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n Observable,\n fromEvent,\n map,\n startWith\n} from \"rxjs\"\n\nimport { getElement } from \"../element\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Toggle\n */\nexport type Toggle =\n | \"drawer\" /* Toggle for drawer */\n | \"search\" /* Toggle for search */\n\n/* ----------------------------------------------------------------------------\n * Data\n * ------------------------------------------------------------------------- */\n\n/**\n * Toggle map\n */\nconst toggles: Record = {\n drawer: getElement(\"[data-md-toggle=drawer]\"),\n search: getElement(\"[data-md-toggle=search]\")\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Retrieve the value of a toggle\n *\n * @param name - Toggle\n *\n * @returns Toggle value\n */\nexport function getToggle(name: Toggle): boolean {\n return toggles[name].checked\n}\n\n/**\n * Set toggle\n *\n * Simulating a click event seems to be the most cross-browser compatible way\n * of changing the value while also emitting a `change` event. Before, Material\n * used `CustomEvent` to programmatically change the value of a toggle, but this\n * is a much simpler and cleaner solution which doesn't require a polyfill.\n *\n * @param name - Toggle\n * @param value - Toggle value\n */\nexport function setToggle(name: Toggle, value: boolean): void {\n if (toggles[name].checked !== value)\n toggles[name].click()\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Watch toggle\n *\n * @param name - Toggle\n *\n * @returns Toggle value observable\n */\nexport function watchToggle(name: Toggle): Observable {\n const el = toggles[name]\n return fromEvent(el, \"change\")\n .pipe(\n map(() => el.checked),\n startWith(el.checked)\n )\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n Observable,\n filter,\n fromEvent,\n map,\n share\n} from \"rxjs\"\n\nimport { getActiveElement } from \"../element\"\nimport { getToggle } from \"../toggle\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Keyboard mode\n */\nexport type KeyboardMode =\n | \"global\" /* Global */\n | \"search\" /* Search is open */\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Keyboard\n */\nexport interface Keyboard {\n mode: KeyboardMode /* Keyboard mode */\n type: string /* Key type */\n claim(): void /* Key claim */\n}\n\n/* ----------------------------------------------------------------------------\n * Helper functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Check whether an element may receive keyboard input\n *\n * @param el - Element\n * @param type - Key type\n *\n * @returns Test result\n */\nfunction isSusceptibleToKeyboard(\n el: HTMLElement, type: string\n): boolean {\n switch (el.constructor) {\n\n /* Input elements */\n case HTMLInputElement:\n /* @ts-expect-error - omit unnecessary type cast */\n if (el.type === \"radio\")\n return /^Arrow/.test(type)\n else\n return true\n\n /* Select element and textarea */\n case HTMLSelectElement:\n case HTMLTextAreaElement:\n return true\n\n /* Everything else */\n default:\n return el.isContentEditable\n }\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch keyboard\n *\n * @returns Keyboard observable\n */\nexport function watchKeyboard(): Observable {\n return fromEvent(window, \"keydown\")\n .pipe(\n filter(ev => !(ev.metaKey || ev.ctrlKey)),\n map(ev => ({\n mode: getToggle(\"search\") ? \"search\" : \"global\",\n type: ev.key,\n claim() {\n ev.preventDefault()\n ev.stopPropagation()\n }\n } as Keyboard)),\n filter(({ mode, type }) => {\n if (mode === \"global\") {\n const active = getActiveElement()\n if (typeof active !== \"undefined\")\n return !isSusceptibleToKeyboard(active, type)\n }\n return true\n }),\n share()\n )\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { Subject } from \"rxjs\"\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Retrieve location\n *\n * This function returns a `URL` object (and not `Location`) to normalize the\n * typings across the application. Furthermore, locations need to be tracked\n * without setting them and `Location` is a singleton which represents the\n * current location.\n *\n * @returns URL\n */\nexport function getLocation(): URL {\n return new URL(location.href)\n}\n\n/**\n * Set location\n *\n * @param url - URL to change to\n */\nexport function setLocation(url: URL): void {\n location.href = url.href\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Watch location\n *\n * @returns Location subject\n */\nexport function watchLocation(): Subject {\n return new Subject()\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { JSX as JSXInternal } from \"preact\"\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * HTML attributes\n */\ntype Attributes =\n & JSXInternal.HTMLAttributes\n & JSXInternal.SVGAttributes\n & Record\n\n/**\n * Child element\n */\ntype Child =\n | HTMLElement\n | Text\n | string\n | number\n\n/* ----------------------------------------------------------------------------\n * Helper functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Append a child node to an element\n *\n * @param el - Element\n * @param child - Child node(s)\n */\nfunction appendChild(el: HTMLElement, child: Child | Child[]): void {\n\n /* Handle primitive types (including raw HTML) */\n if (typeof child === \"string\" || typeof child === \"number\") {\n el.innerHTML += child.toString()\n\n /* Handle nodes */\n } else if (child instanceof Node) {\n el.appendChild(child)\n\n /* Handle nested children */\n } else if (Array.isArray(child)) {\n for (const node of child)\n appendChild(el, node)\n }\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * JSX factory\n *\n * @template T - Element type\n *\n * @param tag - HTML tag\n * @param attributes - HTML attributes\n * @param children - Child elements\n *\n * @returns Element\n */\nexport function h(\n tag: T, attributes?: Attributes | null, ...children: Child[]\n): HTMLElementTagNameMap[T]\n\nexport function h(\n tag: string, attributes?: Attributes | null, ...children: Child[]\n): T\n\nexport function h(\n tag: string, attributes?: Attributes | null, ...children: Child[]\n): T {\n const el = document.createElement(tag)\n\n /* Set attributes, if any */\n if (attributes)\n for (const attr of Object.keys(attributes))\n if (typeof attributes[attr] !== \"boolean\")\n el.setAttribute(attr, attributes[attr])\n else if (attributes[attr])\n el.setAttribute(attr, \"\")\n\n /* Append child nodes */\n for (const child of children)\n appendChild(el, child)\n\n /* Return element */\n return el as T\n}\n\n/* ----------------------------------------------------------------------------\n * Namespace\n * ------------------------------------------------------------------------- */\n\nexport declare namespace h {\n namespace JSX {\n type Element = HTMLElement\n type IntrinsicElements = JSXInternal.IntrinsicElements\n }\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Truncate a string after the given number of characters\n *\n * This is not a very reasonable approach, since the summaries kind of suck.\n * It would be better to create something more intelligent, highlighting the\n * search occurrences and making a better summary out of it, but this note was\n * written three years ago, so who knows if we'll ever fix it.\n *\n * @param value - Value to be truncated\n * @param n - Number of characters\n *\n * @returns Truncated value\n */\nexport function truncate(value: string, n: number): string {\n let i = n\n if (value.length > i) {\n while (value[i] !== \" \" && --i > 0) { /* keep eating */ }\n return `${value.substring(0, i)}...`\n }\n return value\n}\n\n/**\n * Round a number for display with repository facts\n *\n * This is a reverse-engineered version of GitHub's weird rounding algorithm\n * for stars, forks and all other numbers. While all numbers below `1,000` are\n * returned as-is, bigger numbers are converted to fixed numbers:\n *\n * - `1,049` => `1k`\n * - `1,050` => `1.1k`\n * - `1,949` => `1.9k`\n * - `1,950` => `2k`\n *\n * @param value - Original value\n *\n * @returns Rounded value\n */\nexport function round(value: number): string {\n if (value > 999) {\n const digits = +((value - 950) % 1000 > 99)\n return `${((value + 0.000001) / 1000).toFixed(digits)}k`\n } else {\n return value.toString()\n }\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n Observable,\n filter,\n fromEvent,\n map,\n shareReplay,\n startWith\n} from \"rxjs\"\n\nimport { getOptionalElement } from \"~/browser\"\nimport { h } from \"~/utilities\"\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Retrieve location hash\n *\n * @returns Location hash\n */\nexport function getLocationHash(): string {\n return location.hash.substring(1)\n}\n\n/**\n * Set location hash\n *\n * Setting a new fragment identifier via `location.hash` will have no effect\n * if the value doesn't change. When a new fragment identifier is set, we want\n * the browser to target the respective element at all times, which is why we\n * use this dirty little trick.\n *\n * @param hash - Location hash\n */\nexport function setLocationHash(hash: string): void {\n const el = h(\"a\", { href: hash })\n el.addEventListener(\"click\", ev => ev.stopPropagation())\n el.click()\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Watch location hash\n *\n * @returns Location hash observable\n */\nexport function watchLocationHash(): Observable {\n return fromEvent(window, \"hashchange\")\n .pipe(\n map(getLocationHash),\n startWith(getLocationHash()),\n filter(hash => hash.length > 0),\n shareReplay(1)\n )\n}\n\n/**\n * Watch location target\n *\n * @returns Location target observable\n */\nexport function watchLocationTarget(): Observable {\n return watchLocationHash()\n .pipe(\n map(id => getOptionalElement(`[id=\"${id}\"]`)!),\n filter(el => typeof el !== \"undefined\")\n )\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n EMPTY,\n Observable,\n fromEvent,\n fromEventPattern,\n mapTo,\n merge,\n startWith,\n switchMap\n} from \"rxjs\"\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch media query\n *\n * Note that although `MediaQueryList.addListener` is deprecated we have to\n * use it, because it's the only way to ensure proper downward compatibility.\n *\n * @see https://bit.ly/3dUBH2m - GitHub issue\n *\n * @param query - Media query\n *\n * @returns Media observable\n */\nexport function watchMedia(query: string): Observable {\n const media = matchMedia(query)\n return fromEventPattern(next => (\n media.addListener(() => next(media.matches))\n ))\n .pipe(\n startWith(media.matches)\n )\n}\n\n/**\n * Watch print mode\n *\n * @returns Print observable\n */\nexport function watchPrint(): Observable {\n const media = matchMedia(\"print\")\n return merge(\n fromEvent(window, \"beforeprint\").pipe(mapTo(true)),\n fromEvent(window, \"afterprint\").pipe(mapTo(false))\n )\n .pipe(\n startWith(media.matches)\n )\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Toggle an observable with a media observable\n *\n * @template T - Data type\n *\n * @param query$ - Media observable\n * @param factory - Observable factory\n *\n * @returns Toggled observable\n */\nexport function at(\n query$: Observable, factory: () => Observable\n): Observable {\n return query$\n .pipe(\n switchMap(active => active ? factory() : EMPTY)\n )\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n EMPTY,\n Observable,\n catchError,\n filter,\n from,\n map,\n shareReplay,\n switchMap\n} from \"rxjs\"\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Fetch the given URL\n *\n * If the request fails (e.g. when dispatched from `file://` locations), the\n * observable will complete without emitting a value.\n *\n * @param url - Request URL\n * @param options - Options\n *\n * @returns Response observable\n */\nexport function request(\n url: URL | string, options: RequestInit = { credentials: \"same-origin\" }\n): Observable {\n return from(fetch(`${url}`, options))\n .pipe(\n filter(res => res.status === 200),\n catchError(() => EMPTY)\n )\n}\n\n/**\n * Fetch JSON from the given URL\n *\n * @template T - Data type\n *\n * @param url - Request URL\n * @param options - Options\n *\n * @returns Data observable\n */\nexport function requestJSON(\n url: URL | string, options?: RequestInit\n): Observable {\n return request(url, options)\n .pipe(\n switchMap(res => res.json()),\n shareReplay(1)\n )\n}\n\n/**\n * Fetch XML from the given URL\n *\n * @param url - Request URL\n * @param options - Options\n *\n * @returns Data observable\n */\nexport function requestXML(\n url: URL | string, options?: RequestInit\n): Observable {\n const dom = new DOMParser()\n return request(url, options)\n .pipe(\n switchMap(res => res.text()),\n map(res => dom.parseFromString(res, \"text/xml\")),\n shareReplay(1)\n )\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n Observable,\n defer,\n finalize,\n fromEvent,\n mapTo,\n merge,\n switchMap,\n take,\n throwError\n} from \"rxjs\"\n\nimport { h } from \"~/utilities\"\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Create and load a `script` element\n *\n * This function returns an observable that will emit when the script was\n * successfully loaded, or throw an error if it didn't.\n *\n * @param src - Script URL\n *\n * @returns Script observable\n */\nexport function watchScript(src: string): Observable {\n const script = h(\"script\", { src })\n return defer(() => {\n document.head.appendChild(script)\n return merge(\n fromEvent(script, \"load\"),\n fromEvent(script, \"error\")\n .pipe(\n switchMap(() => (\n throwError(() => new ReferenceError(`Invalid script: ${src}`))\n ))\n )\n )\n .pipe(\n mapTo(undefined),\n finalize(() => document.head.removeChild(script)),\n take(1)\n )\n })\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n Observable,\n fromEvent,\n map,\n merge,\n startWith\n} from \"rxjs\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Viewport offset\n */\nexport interface ViewportOffset {\n x: number /* Horizontal offset */\n y: number /* Vertical offset */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Retrieve viewport offset\n *\n * On iOS Safari, viewport offset can be negative due to overflow scrolling.\n * As this may induce strange behaviors downstream, we'll just limit it to 0.\n *\n * @returns Viewport offset\n */\nexport function getViewportOffset(): ViewportOffset {\n return {\n x: Math.max(0, scrollX),\n y: Math.max(0, scrollY)\n }\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Watch viewport offset\n *\n * @returns Viewport offset observable\n */\nexport function watchViewportOffset(): Observable {\n return merge(\n fromEvent(window, \"scroll\", { passive: true }),\n fromEvent(window, \"resize\", { passive: true })\n )\n .pipe(\n map(getViewportOffset),\n startWith(getViewportOffset())\n )\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n Observable,\n fromEvent,\n map,\n startWith\n} from \"rxjs\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Viewport size\n */\nexport interface ViewportSize {\n width: number /* Viewport width */\n height: number /* Viewport height */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Retrieve viewport size\n *\n * @returns Viewport size\n */\nexport function getViewportSize(): ViewportSize {\n return {\n width: innerWidth,\n height: innerHeight\n }\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Watch viewport size\n *\n * @returns Viewport size observable\n */\nexport function watchViewportSize(): Observable {\n return fromEvent(window, \"resize\", { passive: true })\n .pipe(\n map(getViewportSize),\n startWith(getViewportSize())\n )\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n Observable,\n combineLatest,\n map,\n shareReplay\n} from \"rxjs\"\n\nimport {\n ViewportOffset,\n watchViewportOffset\n} from \"../offset\"\nimport {\n ViewportSize,\n watchViewportSize\n} from \"../size\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Viewport\n */\nexport interface Viewport {\n offset: ViewportOffset /* Viewport offset */\n size: ViewportSize /* Viewport size */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch viewport\n *\n * @returns Viewport observable\n */\nexport function watchViewport(): Observable {\n return combineLatest([\n watchViewportOffset(),\n watchViewportSize()\n ])\n .pipe(\n map(([offset, size]) => ({ offset, size })),\n shareReplay(1)\n )\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n Observable,\n combineLatest,\n distinctUntilKeyChanged,\n map\n} from \"rxjs\"\n\nimport { Header } from \"~/components\"\n\nimport { getElementOffset } from \"../../element\"\nimport { Viewport } from \"../_\"\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch options\n */\ninterface WatchOptions {\n viewport$: Observable /* Viewport observable */\n header$: Observable
/* Header observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch viewport relative to element\n *\n * @param el - Element\n * @param options - Options\n *\n * @returns Viewport observable\n */\nexport function watchViewportAt(\n el: HTMLElement, { viewport$, header$ }: WatchOptions\n): Observable {\n const size$ = viewport$\n .pipe(\n distinctUntilKeyChanged(\"size\")\n )\n\n /* Compute element offset */\n const offset$ = combineLatest([size$, header$])\n .pipe(\n map(() => getElementOffset(el))\n )\n\n /* Compute relative viewport, return hot observable */\n return combineLatest([header$, viewport$, offset$])\n .pipe(\n map(([{ height }, { offset, size }, { x, y }]) => ({\n offset: {\n x: offset.x - x,\n y: offset.y - y + height\n },\n size\n }))\n )\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n Observable,\n Subject,\n fromEvent,\n map,\n share,\n switchMapTo,\n tap,\n throttle\n} from \"rxjs\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Worker message\n */\nexport interface WorkerMessage {\n type: unknown /* Message type */\n data?: unknown /* Message data */\n}\n\n/**\n * Worker handler\n *\n * @template T - Message type\n */\nexport interface WorkerHandler<\n T extends WorkerMessage\n> {\n tx$: Subject /* Message transmission subject */\n rx$: Observable /* Message receive observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch options\n *\n * @template T - Worker message type\n */\ninterface WatchOptions {\n tx$: Observable /* Message transmission observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch a web worker\n *\n * This function returns an observable that sends all values emitted by the\n * message observable to the web worker. Web worker communication is expected\n * to be bidirectional (request-response) and synchronous. Messages that are\n * emitted during a pending request are throttled, the last one is emitted.\n *\n * @param worker - Web worker\n * @param options - Options\n *\n * @returns Worker message observable\n */\nexport function watchWorker(\n worker: Worker, { tx$ }: WatchOptions\n): Observable {\n\n /* Intercept messages from worker-like objects */\n const rx$ = fromEvent(worker, \"message\")\n .pipe(\n map(({ data }) => data as T)\n )\n\n /* Send and receive messages, return hot observable */\n return tx$\n .pipe(\n throttle(() => rx$, { leading: true, trailing: true }),\n tap(message => worker.postMessage(message)),\n switchMapTo(rx$),\n share()\n )\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { getElement, getLocation } from \"~/browser\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Feature flag\n */\nexport type Flag =\n | \"content.code.annotate\" /* Code annotations */\n | \"header.autohide\" /* Hide header */\n | \"navigation.expand\" /* Automatic expansion */\n | \"navigation.indexes\" /* Section pages */\n | \"navigation.instant\" /* Instant loading */\n | \"navigation.sections\" /* Section navigation */\n | \"navigation.tabs\" /* Tabs navigation */\n | \"navigation.tabs.sticky\" /* Tabs navigation (sticky) */\n | \"navigation.top\" /* Back-to-top button */\n | \"navigation.tracking\" /* Anchor tracking */\n | \"search.highlight\" /* Search highlighting */\n | \"search.share\" /* Search sharing */\n | \"search.suggest\" /* Search suggestions */\n | \"toc.integrate\" /* Integrated table of contents */\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Translation\n */\nexport type Translation =\n | \"clipboard.copy\" /* Copy to clipboard */\n | \"clipboard.copied\" /* Copied to clipboard */\n | \"search.config.lang\" /* Search language */\n | \"search.config.pipeline\" /* Search pipeline */\n | \"search.config.separator\" /* Search separator */\n | \"search.placeholder\" /* Search */\n | \"search.result.placeholder\" /* Type to start searching */\n | \"search.result.none\" /* No matching documents */\n | \"search.result.one\" /* 1 matching document */\n | \"search.result.other\" /* # matching documents */\n | \"search.result.more.one\" /* 1 more on this page */\n | \"search.result.more.other\" /* # more on this page */\n | \"search.result.term.missing\" /* Missing */\n | \"select.version.title\" /* Version selector */\n\n/**\n * Translations\n */\nexport type Translations = Record\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Versioning\n */\nexport interface Versioning {\n provider: \"mike\" /* Version provider */\n default?: string /* Default version */\n}\n\n/**\n * Configuration\n */\nexport interface Config {\n base: string /* Base URL */\n features: Flag[] /* Feature flags */\n translations: Translations /* Translations */\n search: string /* Search worker URL */\n version?: Versioning /* Versioning */\n}\n\n/* ----------------------------------------------------------------------------\n * Data\n * ------------------------------------------------------------------------- */\n\n/**\n * Retrieve global configuration and make base URL absolute\n */\nconst script = getElement(\"#__config\")\nconst config: Config = JSON.parse(script.textContent!)\nconfig.base = `${new URL(config.base, getLocation())}`\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Retrieve global configuration\n *\n * @returns Global configuration\n */\nexport function configuration(): Config {\n return config\n}\n\n/**\n * Check whether a feature flag is enabled\n *\n * @param flag - Feature flag\n *\n * @returns Test result\n */\nexport function feature(flag: Flag): boolean {\n return config.features.includes(flag)\n}\n\n/**\n * Retrieve the translation for the given key\n *\n * @param key - Key to be translated\n * @param value - Positional value, if any\n *\n * @returns Translation\n */\nexport function translation(\n key: Translation, value?: string | number\n): string {\n return typeof value !== \"undefined\"\n ? config.translations[key].replace(\"#\", value.toString())\n : config.translations[key]\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { getElement, getElements } from \"~/browser\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Component type\n */\nexport type ComponentType =\n | \"announce\" /* Announcement bar */\n | \"container\" /* Container */\n | \"content\" /* Content */\n | \"dialog\" /* Dialog */\n | \"header\" /* Header */\n | \"header-title\" /* Header title */\n | \"header-topic\" /* Header topic */\n | \"main\" /* Main area */\n | \"outdated\" /* Version warning */\n | \"palette\" /* Color palette */\n | \"search\" /* Search */\n | \"search-query\" /* Search input */\n | \"search-result\" /* Search results */\n | \"search-share\" /* Search sharing */\n | \"search-suggest\" /* Search suggestions */\n | \"sidebar\" /* Sidebar */\n | \"skip\" /* Skip link */\n | \"source\" /* Repository information */\n | \"tabs\" /* Navigation tabs */\n | \"toc\" /* Table of contents */\n | \"top\" /* Back-to-top button */\n\n/**\n * Component\n *\n * @template T - Component type\n * @template U - Reference type\n */\nexport type Component<\n T extends {} = {},\n U extends HTMLElement = HTMLElement\n> =\n T & {\n ref: U /* Component reference */\n }\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Component type map\n */\ninterface ComponentTypeMap {\n \"announce\": HTMLElement /* Announcement bar */\n \"container\": HTMLElement /* Container */\n \"content\": HTMLElement /* Content */\n \"dialog\": HTMLElement /* Dialog */\n \"header\": HTMLElement /* Header */\n \"header-title\": HTMLElement /* Header title */\n \"header-topic\": HTMLElement /* Header topic */\n \"main\": HTMLElement /* Main area */\n \"outdated\": HTMLElement /* Version warning */\n \"palette\": HTMLElement /* Color palette */\n \"search\": HTMLElement /* Search */\n \"search-query\": HTMLInputElement /* Search input */\n \"search-result\": HTMLElement /* Search results */\n \"search-share\": HTMLAnchorElement /* Search sharing */\n \"search-suggest\": HTMLElement /* Search suggestions */\n \"sidebar\": HTMLElement /* Sidebar */\n \"skip\": HTMLAnchorElement /* Skip link */\n \"source\": HTMLAnchorElement /* Repository information */\n \"tabs\": HTMLElement /* Navigation tabs */\n \"toc\": HTMLElement /* Table of contents */\n \"top\": HTMLAnchorElement /* Back-to-top button */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Retrieve the element for a given component or throw a reference error\n *\n * @template T - Component type\n *\n * @param type - Component type\n * @param node - Node of reference\n *\n * @returns Element\n */\nexport function getComponentElement(\n type: T, node: ParentNode = document\n): ComponentTypeMap[T] {\n return getElement(`[data-md-component=${type}]`, node)\n}\n\n/**\n * Retrieve all elements for a given component\n *\n * @template T - Component type\n *\n * @param type - Component type\n * @param node - Node of reference\n *\n * @returns Elements\n */\nexport function getComponentElements(\n type: T, node: ParentNode = document\n): ComponentTypeMap[T][] {\n return getElements(`[data-md-component=${type}]`, node)\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport ClipboardJS from \"clipboard\"\nimport {\n EMPTY,\n Observable,\n Subject,\n defer,\n distinctUntilChanged,\n distinctUntilKeyChanged,\n filter,\n finalize,\n map,\n mergeWith,\n switchMap,\n take,\n takeLast,\n takeUntil,\n tap\n} from \"rxjs\"\n\nimport { feature } from \"~/_\"\nimport {\n getElementContentSize,\n watchElementSize,\n watchElementVisibility\n} from \"~/browser\"\nimport { renderClipboardButton } from \"~/templates\"\n\nimport { Component } from \"../../../_\"\nimport {\n Annotation,\n mountAnnotationList\n} from \"../../annotation\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Code block\n */\nexport interface CodeBlock {\n scrollable: boolean /* Code block overflows */\n}\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Mount options\n */\ninterface MountOptions {\n print$: Observable /* Media print observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Data\n * ------------------------------------------------------------------------- */\n\n/**\n * Global sequence number for Clipboard.js integration\n */\nlet sequence = 0\n\n/* ----------------------------------------------------------------------------\n * Helper functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Find candidate list element directly following a code block\n *\n * @param el - Code block element\n *\n * @returns List element or nothing\n */\nfunction findCandidateList(el: HTMLElement): HTMLElement | undefined {\n if (el.nextElementSibling) {\n const sibling = el.nextElementSibling as HTMLElement\n if (sibling.tagName === \"OL\")\n return sibling\n\n /* Skip empty paragraphs - see https://bit.ly/3r4ZJ2O */\n else if (sibling.tagName === \"P\" && !sibling.children.length)\n return findCandidateList(sibling)\n }\n\n /* Everything else */\n return undefined\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch code block\n *\n * This function monitors size changes of the viewport, as well as switches of\n * content tabs with embedded code blocks, as both may trigger overflow.\n *\n * @param el - Code block element\n *\n * @returns Code block observable\n */\nexport function watchCodeBlock(\n el: HTMLElement\n): Observable {\n return watchElementSize(el)\n .pipe(\n map(({ width }) => {\n const content = getElementContentSize(el)\n return {\n scrollable: content.width > width\n }\n }),\n distinctUntilKeyChanged(\"scrollable\")\n )\n}\n\n/**\n * Mount code block\n *\n * This function ensures that an overflowing code block is focusable through\n * keyboard, so it can be scrolled without a mouse to improve on accessibility.\n * Furthermore, if code annotations are enabled, they are mounted if and only\n * if the code block is currently visible, e.g., not in a hidden content tab.\n *\n * @param el - Code block element\n * @param options - Options\n *\n * @returns Code block and annotation component observable\n */\nexport function mountCodeBlock(\n el: HTMLElement, options: MountOptions\n): Observable> {\n const { matches: hover } = matchMedia(\"(hover)\")\n\n /* Defer mounting of code block - see https://bit.ly/3vHVoVD */\n const factory$ = defer(() => {\n const push$ = new Subject()\n push$.subscribe(({ scrollable }) => {\n if (scrollable && hover)\n el.setAttribute(\"tabindex\", \"0\")\n else\n el.removeAttribute(\"tabindex\")\n })\n\n /* Render button for Clipboard.js integration */\n if (ClipboardJS.isSupported()) {\n const parent = el.closest(\"pre\")!\n parent.id = `__code_${++sequence}`\n parent.insertBefore(\n renderClipboardButton(parent.id),\n el\n )\n }\n\n /* Handle code annotations */\n const container = el.closest([\n \":not(td):not(.code) > .highlight\",\n \".highlighttable\"\n ].join(\", \"))\n if (container instanceof HTMLElement) {\n const list = findCandidateList(container)\n\n /* Mount code annotations, if enabled */\n if (typeof list !== \"undefined\" && (\n container.classList.contains(\"annotate\") ||\n feature(\"content.code.annotate\")\n )) {\n const annotations$ = mountAnnotationList(list, el, options)\n\n /* Create and return component */\n return watchCodeBlock(el)\n .pipe(\n tap(state => push$.next(state)),\n finalize(() => push$.complete()),\n map(state => ({ ref: el, ...state })),\n mergeWith(\n watchElementSize(container)\n .pipe(\n takeUntil(push$.pipe(takeLast(1))),\n map(({ width, height }) => width && height),\n distinctUntilChanged(),\n switchMap(active => active ? annotations$ : EMPTY)\n )\n )\n )\n }\n }\n\n /* Create and return component */\n return watchCodeBlock(el)\n .pipe(\n tap(state => push$.next(state)),\n finalize(() => push$.complete()),\n map(state => ({ ref: el, ...state }))\n )\n })\n\n /* Mount code block on first sight */\n return watchElementVisibility(el)\n .pipe(\n filter(visible => visible),\n take(1),\n switchMap(() => factory$)\n )\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { h } from \"~/utilities\"\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Render an empty annotation\n *\n * @param id - Annotation identifier\n *\n * @returns Element\n */\nexport function renderAnnotation(id: number): HTMLElement {\n return (\n \n )\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { translation } from \"~/_\"\nimport { h } from \"~/utilities\"\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Render a 'copy-to-clipboard' button\n *\n * @param id - Unique identifier\n *\n * @returns Element\n */\nexport function renderClipboardButton(id: string): HTMLElement {\n return (\n code`}\n >\n )\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { ComponentChild } from \"preact\"\n\nimport { feature, translation } from \"~/_\"\nimport {\n SearchDocument,\n SearchMetadata,\n SearchResultItem\n} from \"~/integrations/search\"\nimport { h, truncate } from \"~/utilities\"\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Render flag\n */\nconst enum Flag {\n TEASER = 1, /* Render teaser */\n PARENT = 2 /* Render as parent */\n}\n\n/* ----------------------------------------------------------------------------\n * Helper function\n * ------------------------------------------------------------------------- */\n\n/**\n * Render a search document\n *\n * @param document - Search document\n * @param flag - Render flags\n *\n * @returns Element\n */\nfunction renderSearchDocument(\n document: SearchDocument & SearchMetadata, flag: Flag\n): HTMLElement {\n const parent = flag & Flag.PARENT\n const teaser = flag & Flag.TEASER\n\n /* Render missing query terms */\n const missing = Object.keys(document.terms)\n .filter(key => !document.terms[key])\n .reduce((list, key) => [\n ...list, {key}, \" \"\n ], [])\n .slice(0, -1)\n\n /* Assemble query string for highlighting */\n const url = new URL(document.location)\n if (feature(\"search.highlight\"))\n url.searchParams.set(\"h\", Object.entries(document.terms)\n .filter(([, match]) => match)\n .reduce((highlight, [value]) => `${highlight} ${value}`.trim(), \"\")\n )\n\n /* Render article or section, depending on flags */\n return (\n \n \n {parent > 0 &&
}\n

{document.title}

\n {teaser > 0 && document.text.length > 0 &&\n

\n {truncate(document.text, 320)}\n

\n }\n {document.tags && document.tags.map(tag => (\n {tag}\n ))}\n {teaser > 0 && missing.length > 0 &&\n

\n {translation(\"search.result.term.missing\")}: {...missing}\n

\n }\n \n
\n )\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Render a search result\n *\n * @param result - Search result\n *\n * @returns Element\n */\nexport function renderSearchResultItem(\n result: SearchResultItem\n): HTMLElement {\n const threshold = result[0].score\n const docs = [...result]\n\n /* Find and extract parent article */\n const parent = docs.findIndex(doc => !doc.location.includes(\"#\"))\n const [article] = docs.splice(parent, 1)\n\n /* Determine last index above threshold */\n let index = docs.findIndex(doc => doc.score < threshold)\n if (index === -1)\n index = docs.length\n\n /* Partition sections */\n const best = docs.slice(0, index)\n const more = docs.slice(index)\n\n /* Render children */\n const children = [\n renderSearchDocument(article, Flag.PARENT | +(!parent && index === 0)),\n ...best.map(section => renderSearchDocument(section, Flag.TEASER)),\n ...more.length ? [\n
\n \n {more.length > 0 && more.length === 1\n ? translation(\"search.result.more.one\")\n : translation(\"search.result.more.other\", more.length)\n }\n \n {...more.map(section => renderSearchDocument(section, Flag.TEASER))}\n
\n ] : []\n ]\n\n /* Render search result */\n return (\n
  • \n {children}\n
  • \n )\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { SourceFacts } from \"~/components\"\nimport { h, round } from \"~/utilities\"\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Render repository facts\n *\n * @param facts - Repository facts\n *\n * @returns Element\n */\nexport function renderSourceFacts(facts: SourceFacts): HTMLElement {\n return (\n
      \n {Object.entries(facts).map(([key, value]) => (\n
    • \n {typeof value === \"number\" ? round(value) : value}\n
    • \n ))}\n
    \n )\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { h } from \"~/utilities\"\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Render a table inside a wrapper to improve scrolling on mobile\n *\n * @param table - Table element\n *\n * @returns Element\n */\nexport function renderTable(table: HTMLElement): HTMLElement {\n return (\n
    \n
    \n {table}\n
    \n
    \n )\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { configuration, translation } from \"~/_\"\nimport { h } from \"~/utilities\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Version\n */\nexport interface Version {\n version: string /* Version identifier */\n title: string /* Version title */\n aliases: string[] /* Version aliases */\n}\n\n/* ----------------------------------------------------------------------------\n * Helper functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Render a version\n *\n * @param version - Version\n *\n * @returns Element\n */\nfunction renderVersion(version: Version): HTMLElement {\n const config = configuration()\n\n /* Ensure trailing slash, see https://bit.ly/3rL5u3f */\n const url = new URL(`../${version.version}/`, config.base)\n return (\n
  • \n \n {version.title}\n \n
  • \n )\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Render a version selector\n *\n * @param versions - Versions\n * @param active - Active version\n *\n * @returns Element\n */\nexport function renderVersionSelector(\n versions: Version[], active: Version\n): HTMLElement {\n return (\n
    \n \n {active.title}\n \n
      \n {versions.map(renderVersion)}\n
    \n
    \n )\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n EMPTY,\n Observable,\n Subject,\n animationFrameScheduler,\n combineLatest,\n defer,\n finalize,\n fromEvent,\n map,\n switchMap,\n take,\n tap,\n throttleTime\n} from \"rxjs\"\n\nimport {\n ElementOffset,\n getElement,\n getElementSize,\n watchElementContentOffset,\n watchElementFocus,\n watchElementOffset\n} from \"~/browser\"\n\nimport { Component } from \"../../../_\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Annotation\n */\nexport interface Annotation {\n active: boolean /* Annotation is active */\n offset: ElementOffset /* Annotation offset */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch annotation\n *\n * @param el - Annotation element\n * @param container - Containing element\n *\n * @returns Annotation observable\n */\nexport function watchAnnotation(\n el: HTMLElement, container: HTMLElement\n): Observable {\n const offset$ = defer(() => combineLatest([\n watchElementOffset(el),\n watchElementContentOffset(container)\n ]))\n .pipe(\n map(([{ x, y }, scroll]) => {\n const { width } = getElementSize(el)\n return ({\n x: x - scroll.x + width / 2,\n y: y - scroll.y\n })\n })\n )\n\n /* Actively watch annotation on focus */\n return watchElementFocus(el)\n .pipe(\n switchMap(active => offset$\n .pipe(\n map(offset => ({ active, offset })),\n take(+!active || Infinity)\n )\n )\n )\n}\n\n/**\n * Mount annotation\n *\n * @param el - Annotation element\n * @param container - Containing element\n *\n * @returns Annotation component observable\n */\nexport function mountAnnotation(\n el: HTMLElement, container: HTMLElement\n): Observable> {\n return defer(() => {\n const push$ = new Subject()\n push$.subscribe({\n\n /* Handle emission */\n next({ offset }) {\n el.style.setProperty(\"--md-tooltip-x\", `${offset.x}px`)\n el.style.setProperty(\"--md-tooltip-y\", `${offset.y}px`)\n },\n\n /* Handle complete */\n complete() {\n el.style.removeProperty(\"--md-tooltip-x\")\n el.style.removeProperty(\"--md-tooltip-y\")\n }\n })\n\n /* Track relative origin of tooltip */\n push$\n .pipe(\n throttleTime(500, animationFrameScheduler),\n map(() => container.getBoundingClientRect()),\n map(({ x }) => x)\n )\n .subscribe({\n\n /* Handle emission */\n next(origin) {\n if (origin)\n el.style.setProperty(\"--md-tooltip-0\", `${-origin}px`)\n else\n el.style.removeProperty(\"--md-tooltip-0\")\n },\n\n /* Handle complete */\n complete() {\n el.style.removeProperty(\"--md-tooltip-0\")\n }\n })\n\n /* Close open annotation on click */\n const index = getElement(\":scope > :last-child\", el)\n const blur$ = fromEvent(index, \"mousedown\", { once: true })\n push$\n .pipe(\n switchMap(({ active }) => active ? blur$ : EMPTY),\n tap(ev => ev.preventDefault())\n )\n .subscribe(() => el.blur())\n\n /* Create and return component */\n return watchAnnotation(el, container)\n .pipe(\n tap(state => push$.next(state)),\n finalize(() => push$.complete()),\n map(state => ({ ref: el, ...state }))\n )\n })\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n EMPTY,\n Observable,\n Subject,\n defer,\n finalize,\n merge,\n share,\n takeLast,\n takeUntil\n} from \"rxjs\"\n\nimport {\n getElement,\n getElements,\n getOptionalElement\n} from \"~/browser\"\nimport { renderAnnotation } from \"~/templates\"\n\nimport { Component } from \"../../../_\"\nimport {\n Annotation,\n mountAnnotation\n} from \"../_\"\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Mount options\n */\ninterface MountOptions {\n print$: Observable /* Media print observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Helper functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Find all annotation markers in the given code block\n *\n * @param container - Containing element\n *\n * @returns Annotation markers\n */\nfunction findAnnotationMarkers(container: HTMLElement): Text[] {\n const markers: Text[] = []\n for (const comment of getElements(\".c, .c1, .cm\", container)) {\n let match: RegExpExecArray | null\n\n /* Split text at marker and add to list */\n let text = comment.firstChild as Text\n if (text instanceof Text)\n while ((match = /\\((\\d+)\\)/.exec(text.textContent!))) {\n const marker = text.splitText(match.index)\n text = marker.splitText(match[0].length)\n markers.push(marker)\n }\n }\n return markers\n}\n\n/**\n * Swap the child nodes of two elements\n *\n * @param source - Source element\n * @param target - Target element\n */\nfunction swap(source: HTMLElement, target: HTMLElement): void {\n target.append(...Array.from(source.childNodes))\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Mount annotation list\n *\n * This function analyzes the containing code block and checks for markers\n * referring to elements in the given annotation list. If no markers are found,\n * the list is left untouched. Otherwise, list elements are rendered as\n * annotations inside the code block.\n *\n * @param el - Annotation list element\n * @param container - Containing element\n * @param options - Options\n *\n * @returns Annotation component observable\n */\nexport function mountAnnotationList(\n el: HTMLElement, container: HTMLElement, { print$ }: MountOptions\n): Observable> {\n\n /* Find and replace all markers with empty annotations */\n const annotations = new Map()\n for (const marker of findAnnotationMarkers(container)) {\n const [, id] = marker.textContent!.match(/\\((\\d+)\\)/)!\n if (getOptionalElement(`li:nth-child(${id})`, el)) {\n annotations.set(+id, renderAnnotation(+id))\n marker.replaceWith(annotations.get(+id)!)\n }\n }\n\n /* Keep list if there are no annotations to render */\n if (annotations.size === 0)\n return EMPTY\n\n /* Create and return component */\n return defer(() => {\n const done$ = new Subject()\n\n /* Handle print mode - see https://bit.ly/3rgPdpt */\n print$\n .pipe(\n takeUntil(done$.pipe(takeLast(1)))\n )\n .subscribe(active => {\n el.hidden = !active\n\n /* Show annotations in code block or list (print) */\n for (const [id, annotation] of annotations) {\n const inner = getElement(\".md-typeset\", annotation)\n const child = getElement(`li:nth-child(${id})`, el)\n if (!active)\n swap(child, inner)\n else\n swap(inner, child)\n }\n })\n\n /* Create and return component */\n return merge(...[...annotations]\n .map(([, annotation]) => (\n mountAnnotation(annotation, container)\n ))\n )\n .pipe(\n finalize(() => done$.complete()),\n share()\n )\n })\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n Observable,\n mapTo,\n of,\n shareReplay,\n tap\n} from \"rxjs\"\n\nimport { watchScript } from \"~/browser\"\nimport { h } from \"~/utilities\"\n\nimport { Component } from \"../../../_\"\n\nimport themeCSS from \"./index.css\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Mermaid diagram\n */\nexport interface Mermaid {}\n\n/* ----------------------------------------------------------------------------\n * Data\n * ------------------------------------------------------------------------- */\n\n/**\n * Mermaid instance observable\n */\nlet mermaid$: Observable\n\n/**\n * Global index for Mermaid integration\n */\nlet index = 0\n\n/* ----------------------------------------------------------------------------\n * Helper functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Fetch Mermaid script\n *\n * @returns Mermaid scripts observable\n */\nfunction fetchScripts(): Observable {\n return typeof mermaid === \"undefined\" || mermaid instanceof Element\n ? watchScript(\"https://unpkg.com/mermaid@8.13.3/dist/mermaid.min.js\")\n : of(undefined)\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Mount Mermaid diagram\n *\n * @param el - Code block element\n *\n * @returns Mermaid diagram component observable\n */\nexport function mountMermaid(\n el: HTMLElement\n): Observable> {\n el.classList.remove(\"mermaid\") // Hack: mitigate https://bit.ly/3CiN6Du\n mermaid$ ||= fetchScripts()\n .pipe(\n tap(() => mermaid.initialize({\n startOnLoad: false,\n themeCSS\n })),\n mapTo(undefined),\n shareReplay(1)\n )\n\n /* Render diagram */\n mermaid$.subscribe(() => {\n el.classList.add(\"mermaid\") // Hack: mitigate https://bit.ly/3CiN6Du\n const id = `__mermaid_${index++}`\n const host = h(\"div\", { class: \"mermaid\" })\n mermaid.mermaidAPI.render(id, el.textContent, (svg: string) => {\n\n /* Create a shadow root and inject diagram */\n const shadow = host.attachShadow({ mode: \"closed\" })\n shadow.innerHTML = svg\n\n /* Replace code block with diagram */\n el.replaceWith(host)\n })\n })\n\n /* Create and return component */\n return mermaid$\n .pipe(\n mapTo({ ref: el })\n )\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n Observable,\n Subject,\n defer,\n filter,\n finalize,\n map,\n mapTo,\n merge,\n tap\n} from \"rxjs\"\n\nimport { Component } from \"../../_\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Details\n */\nexport interface Details {\n action: \"open\" | \"close\" /* Details state */\n reveal?: boolean /* Details is revealed */\n}\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch options\n */\ninterface WatchOptions {\n target$: Observable /* Location target observable */\n print$: Observable /* Media print observable */\n}\n\n/**\n * Mount options\n */\ninterface MountOptions {\n target$: Observable /* Location target observable */\n print$: Observable /* Media print observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch details\n *\n * @param el - Details element\n * @param options - Options\n *\n * @returns Details observable\n */\nexport function watchDetails(\n el: HTMLDetailsElement, { target$, print$ }: WatchOptions\n): Observable
    {\n let open = true\n return merge(\n\n /* Open and focus details on location target */\n target$\n .pipe(\n map(target => target.closest(\"details:not([open])\")!),\n filter(details => el === details),\n mapTo
    ({ action: \"open\", reveal: true })\n ),\n\n /* Open details on print and close afterwards */\n print$\n .pipe(\n filter(active => active || !open),\n tap(() => open = el.open),\n map(active => ({\n action: active ? \"open\" : \"close\"\n }) as Details)\n )\n )\n}\n\n/**\n * Mount details\n *\n * This function ensures that `details` tags are opened on anchor jumps and\n * prior to printing, so the whole content of the page is visible.\n *\n * @param el - Details element\n * @param options - Options\n *\n * @returns Details component observable\n */\nexport function mountDetails(\n el: HTMLDetailsElement, options: MountOptions\n): Observable> {\n return defer(() => {\n const push$ = new Subject
    ()\n push$.subscribe(({ action, reveal }) => {\n if (action === \"open\")\n el.setAttribute(\"open\", \"\")\n else\n el.removeAttribute(\"open\")\n if (reveal)\n el.scrollIntoView()\n })\n\n /* Create and return component */\n return watchDetails(el, options)\n .pipe(\n tap(state => push$.next(state)),\n finalize(() => push$.complete()),\n map(state => ({ ref: el, ...state }))\n )\n })\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { Observable, of } from \"rxjs\"\n\nimport { renderTable } from \"~/templates\"\nimport { h } from \"~/utilities\"\n\nimport { Component } from \"../../_\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Data table\n */\nexport interface DataTable {}\n\n/* ----------------------------------------------------------------------------\n * Data\n * ------------------------------------------------------------------------- */\n\n/**\n * Sentinel for replacement\n */\nconst sentinel = h(\"table\")\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Mount data table\n *\n * This function wraps a data table in another scrollable container, so it can\n * be smoothly scrolled on smaller screen sizes and won't break the layout.\n *\n * @param el - Data table element\n *\n * @returns Data table component observable\n */\nexport function mountDataTable(\n el: HTMLElement\n): Observable> {\n el.replaceWith(sentinel)\n sentinel.replaceWith(renderTable(el))\n\n /* Create and return component */\n return of({ ref: el })\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n Observable,\n Subject,\n animationFrameScheduler,\n asyncScheduler,\n auditTime,\n combineLatest,\n defer,\n finalize,\n fromEvent,\n map,\n mapTo,\n merge,\n startWith,\n subscribeOn,\n takeLast,\n takeUntil,\n tap\n} from \"rxjs\"\n\nimport {\n getElement,\n getElementOffset,\n getElementSize,\n getElements,\n watchElementSize\n} from \"~/browser\"\n\nimport { Component } from \"../../_\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Content tabs\n */\nexport interface ContentTabs {\n active: HTMLLabelElement /* Active tab label */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch content tabs\n *\n * @param el - Content tabs element\n *\n * @returns Content tabs observable\n */\nexport function watchContentTabs(\n el: HTMLElement\n): Observable {\n const inputs = getElements(\":scope > input\", el)\n const active = inputs.find(input => input.checked) || inputs[0]\n return merge(...inputs.map(input => fromEvent(input, \"change\")\n .pipe(\n mapTo({\n active: getElement(`label[for=${input.id}]`)\n })\n )\n ))\n .pipe(\n startWith({\n active: getElement(`label[for=${active.id}]`)\n } as ContentTabs)\n )\n}\n\n/**\n * Mount content tabs\n *\n * This function scrolls the active tab into view. While this functionality is\n * provided by browsers as part of `scrollInfoView`, browsers will always also\n * scroll the vertical axis, which we do not want. Thus, we decided to provide\n * this functionality ourselves.\n *\n * @param el - Content tabs element\n *\n * @returns Content tabs component observable\n */\nexport function mountContentTabs(\n el: HTMLElement\n): Observable> {\n const container = getElement(\".tabbed-labels\", el)\n return defer(() => {\n const push$ = new Subject()\n combineLatest([push$, watchElementSize(el)])\n .pipe(\n auditTime(1, animationFrameScheduler),\n takeUntil(push$.pipe(takeLast(1)))\n )\n .subscribe({\n\n /* Handle emission */\n next([{ active }]) {\n const offset = getElementOffset(active)\n const { width } = getElementSize(active)\n\n /* Set tab indicator offset and width */\n el.style.setProperty(\"--md-indicator-x\", `${offset.x}px`)\n el.style.setProperty(\"--md-indicator-width\", `${width}px`)\n\n /* Smoothly scroll container */\n container.scrollTo({\n behavior: \"smooth\",\n left: offset.x\n })\n },\n\n /* Handle complete */\n complete() {\n el.style.removeProperty(\"--md-indicator-x\")\n el.style.removeProperty(\"--md-indicator-width\")\n }\n })\n\n /* Create and return component */\n return watchContentTabs(el)\n .pipe(\n tap(state => push$.next(state)),\n finalize(() => push$.complete()),\n map(state => ({ ref: el, ...state }))\n )\n })\n .pipe(\n subscribeOn(asyncScheduler)\n )\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { Observable, merge } from \"rxjs\"\n\nimport { getElements } from \"~/browser\"\n\nimport { Component } from \"../../_\"\nimport { Annotation } from \"../annotation\"\nimport {\n CodeBlock,\n Mermaid,\n mountCodeBlock,\n mountMermaid\n} from \"../code\"\nimport {\n Details,\n mountDetails\n} from \"../details\"\nimport {\n DataTable,\n mountDataTable\n} from \"../table\"\nimport {\n ContentTabs,\n mountContentTabs\n} from \"../tabs\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Content\n */\nexport type Content =\n | Annotation\n | ContentTabs\n | CodeBlock\n | Mermaid\n | DataTable\n | Details\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Mount options\n */\ninterface MountOptions {\n target$: Observable /* Location target observable */\n print$: Observable /* Media print observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Mount content\n *\n * This function mounts all components that are found in the content of the\n * actual article, including code blocks, data tables and details.\n *\n * @param el - Content element\n * @param options - Options\n *\n * @returns Content component observable\n */\nexport function mountContent(\n el: HTMLElement, { target$, print$ }: MountOptions\n): Observable> {\n return merge(\n\n /* Code blocks */\n ...getElements(\"pre:not(.mermaid) > code\", el)\n .map(child => mountCodeBlock(child, { print$ })),\n\n /* Mermaid diagrams */\n ...getElements(\"pre.mermaid\", el)\n .map(child => mountMermaid(child)),\n\n /* Data tables */\n ...getElements(\"table:not([class])\", el)\n .map(child => mountDataTable(child)),\n\n /* Details */\n ...getElements(\"details\", el)\n .map(child => mountDetails(child, { target$, print$ })),\n\n /* Content tabs */\n ...getElements(\"[data-tabs]\", el)\n .map(child => mountContentTabs(child))\n )\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n Observable,\n Subject,\n defer,\n delay,\n finalize,\n map,\n merge,\n of,\n switchMap,\n tap\n} from \"rxjs\"\n\nimport { getElement } from \"~/browser\"\n\nimport { Component } from \"../_\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Dialog\n */\nexport interface Dialog {\n message: string /* Dialog message */\n active: boolean /* Dialog is active */\n}\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch options\n */\ninterface WatchOptions {\n alert$: Subject /* Alert subject */\n}\n\n/**\n * Mount options\n */\ninterface MountOptions {\n alert$: Subject /* Alert subject */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch dialog\n *\n * @param _el - Dialog element\n * @param options - Options\n *\n * @returns Dialog observable\n */\nexport function watchDialog(\n _el: HTMLElement, { alert$ }: WatchOptions\n): Observable {\n return alert$\n .pipe(\n switchMap(message => merge(\n of(true),\n of(false).pipe(delay(2000))\n )\n .pipe(\n map(active => ({ message, active }))\n )\n )\n )\n}\n\n/**\n * Mount dialog\n *\n * This function reveals the dialog in the right corner when a new alert is\n * emitted through the subject that is passed as part of the options.\n *\n * @param el - Dialog element\n * @param options - Options\n *\n * @returns Dialog component observable\n */\nexport function mountDialog(\n el: HTMLElement, options: MountOptions\n): Observable> {\n const inner = getElement(\".md-typeset\", el)\n return defer(() => {\n const push$ = new Subject()\n push$.subscribe(({ message, active }) => {\n inner.textContent = message\n if (active)\n el.setAttribute(\"data-md-state\", \"open\")\n else\n el.removeAttribute(\"data-md-state\")\n })\n\n /* Create and return component */\n return watchDialog(el, options)\n .pipe(\n tap(state => push$.next(state)),\n finalize(() => push$.complete()),\n map(state => ({ ref: el, ...state }))\n )\n })\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n Observable,\n Subject,\n bufferCount,\n combineLatest,\n combineLatestWith,\n defer,\n distinctUntilChanged,\n distinctUntilKeyChanged,\n filter,\n map,\n of,\n shareReplay,\n startWith,\n switchMap,\n takeLast,\n takeUntil\n} from \"rxjs\"\n\nimport { feature } from \"~/_\"\nimport {\n Viewport,\n watchElementSize,\n watchToggle\n} from \"~/browser\"\n\nimport { Component } from \"../../_\"\nimport { Main } from \"../../main\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Header\n */\nexport interface Header {\n height: number /* Header visible height */\n sticky: boolean /* Header stickyness */\n hidden: boolean /* Header is hidden */\n}\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch options\n */\ninterface WatchOptions {\n viewport$: Observable /* Viewport observable */\n}\n\n/**\n * Mount options\n */\ninterface MountOptions {\n viewport$: Observable /* Viewport observable */\n header$: Observable
    /* Header observable */\n main$: Observable
    /* Main area observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Helper functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Compute whether the header is hidden\n *\n * If the user scrolls past a certain threshold, the header can be hidden when\n * scrolling down, and shown when scrolling up.\n *\n * @param options - Options\n *\n * @returns Toggle observable\n */\nfunction isHidden({ viewport$ }: WatchOptions): Observable {\n if (!feature(\"header.autohide\"))\n return of(false)\n\n /* Compute direction and turning point */\n const direction$ = viewport$\n .pipe(\n map(({ offset: { y } }) => y),\n bufferCount(2, 1),\n map(([a, b]) => [a < b, b] as const),\n distinctUntilKeyChanged(0)\n )\n\n /* Compute whether header should be hidden */\n const hidden$ = combineLatest([viewport$, direction$])\n .pipe(\n filter(([{ offset }, [, y]]) => Math.abs(y - offset.y) > 100),\n map(([, [direction]]) => direction),\n distinctUntilChanged()\n )\n\n /* Compute threshold for hiding */\n const search$ = watchToggle(\"search\")\n return combineLatest([viewport$, search$])\n .pipe(\n map(([{ offset }, search]) => offset.y > 400 && !search),\n distinctUntilChanged(),\n switchMap(active => active ? hidden$ : of(false)),\n startWith(false)\n )\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch header\n *\n * @param el - Header element\n * @param options - Options\n *\n * @returns Header observable\n */\nexport function watchHeader(\n el: HTMLElement, options: WatchOptions\n): Observable
    {\n return defer(() => {\n const styles = getComputedStyle(el)\n return of(\n styles.position === \"sticky\" ||\n styles.position === \"-webkit-sticky\"\n )\n })\n .pipe(\n combineLatestWith(watchElementSize(el), isHidden(options)),\n map(([sticky, { height }, hidden]) => ({\n height: sticky ? height : 0,\n sticky,\n hidden\n })),\n distinctUntilChanged((a, b) => (\n a.sticky === b.sticky &&\n a.height === b.height &&\n a.hidden === b.hidden\n )),\n shareReplay(1)\n )\n}\n\n/**\n * Mount header\n *\n * This function manages the different states of the header, i.e. whether it's\n * hidden or rendered with a shadow. This depends heavily on the main area.\n *\n * @param el - Header element\n * @param options - Options\n *\n * @returns Header component observable\n */\nexport function mountHeader(\n el: HTMLElement, { header$, main$ }: MountOptions\n): Observable> {\n return defer(() => {\n const push$ = new Subject
    ()\n push$\n .pipe(\n distinctUntilKeyChanged(\"active\"),\n combineLatestWith(header$)\n )\n .subscribe(([{ active }, { hidden }]) => {\n if (active)\n el.setAttribute(\"data-md-state\", hidden ? \"hidden\" : \"shadow\")\n else\n el.removeAttribute(\"data-md-state\")\n })\n\n /* Link to main area */\n main$.subscribe(push$)\n\n /* Create and return component */\n return header$\n .pipe(\n takeUntil(push$.pipe(takeLast(1))),\n map(state => ({ ref: el, ...state }))\n )\n })\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n EMPTY,\n Observable,\n Subject,\n defer,\n distinctUntilKeyChanged,\n finalize,\n map,\n tap\n} from \"rxjs\"\n\nimport {\n Viewport,\n getElementSize,\n getOptionalElement,\n watchViewportAt\n} from \"~/browser\"\n\nimport { Component } from \"../../_\"\nimport { Header } from \"../_\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Header\n */\nexport interface HeaderTitle {\n active: boolean /* Header title is active */\n}\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch options\n */\ninterface WatchOptions {\n viewport$: Observable /* Viewport observable */\n header$: Observable
    /* Header observable */\n}\n\n/**\n * Mount options\n */\ninterface MountOptions {\n viewport$: Observable /* Viewport observable */\n header$: Observable
    /* Header observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch header title\n *\n * @param el - Heading element\n * @param options - Options\n *\n * @returns Header title observable\n */\nexport function watchHeaderTitle(\n el: HTMLElement, { viewport$, header$ }: WatchOptions\n): Observable {\n return watchViewportAt(el, { viewport$, header$ })\n .pipe(\n map(({ offset: { y } }) => {\n const { height } = getElementSize(el)\n return {\n active: y >= height\n }\n }),\n distinctUntilKeyChanged(\"active\")\n )\n}\n\n/**\n * Mount header title\n *\n * This function swaps the header title from the site title to the title of the\n * current page when the user scrolls past the first headline.\n *\n * @param el - Header title element\n * @param options - Options\n *\n * @returns Header title component observable\n */\nexport function mountHeaderTitle(\n el: HTMLElement, options: MountOptions\n): Observable> {\n return defer(() => {\n const push$ = new Subject()\n push$.subscribe(({ active }) => {\n if (active)\n el.setAttribute(\"data-md-state\", \"active\")\n else\n el.removeAttribute(\"data-md-state\")\n })\n\n /* Obtain headline, if any */\n const heading = getOptionalElement(\"article h1\")\n if (typeof heading === \"undefined\")\n return EMPTY\n\n /* Create and return component */\n return watchHeaderTitle(heading, options)\n .pipe(\n tap(state => push$.next(state)),\n finalize(() => push$.complete()),\n map(state => ({ ref: el, ...state }))\n )\n })\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n Observable,\n combineLatest,\n distinctUntilChanged,\n distinctUntilKeyChanged,\n map,\n switchMap\n} from \"rxjs\"\n\nimport {\n Viewport,\n watchElementSize\n} from \"~/browser\"\n\nimport { Header } from \"../header\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Main area\n */\nexport interface Main {\n offset: number /* Main area top offset */\n height: number /* Main area visible height */\n active: boolean /* Main area is active */\n}\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch options\n */\ninterface WatchOptions {\n viewport$: Observable /* Viewport observable */\n header$: Observable
    /* Header observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch main area\n *\n * This function returns an observable that computes the visual parameters of\n * the main area which depends on the viewport vertical offset and height, as\n * well as the height of the header element, if the header is fixed.\n *\n * @param el - Main area element\n * @param options - Options\n *\n * @returns Main area observable\n */\nexport function watchMain(\n el: HTMLElement, { viewport$, header$ }: WatchOptions\n): Observable
    {\n\n /* Compute necessary adjustment for header */\n const adjust$ = header$\n .pipe(\n map(({ height }) => height),\n distinctUntilChanged()\n )\n\n /* Compute the main area's top and bottom borders */\n const border$ = adjust$\n .pipe(\n switchMap(() => watchElementSize(el)\n .pipe(\n map(({ height }) => ({\n top: el.offsetTop,\n bottom: el.offsetTop + height\n })),\n distinctUntilKeyChanged(\"bottom\")\n )\n )\n )\n\n /* Compute the main area's offset, visible height and if we scrolled past */\n return combineLatest([adjust$, border$, viewport$])\n .pipe(\n map(([header, { top, bottom }, { offset: { y }, size: { height } }]) => {\n height = Math.max(0, height\n - Math.max(0, top - y, header)\n - Math.max(0, height + y - bottom)\n )\n return {\n offset: top - header,\n height,\n active: top - header <= y\n }\n }),\n distinctUntilChanged((a, b) => (\n a.offset === b.offset &&\n a.height === b.height &&\n a.active === b.active\n ))\n )\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n Observable,\n Subject,\n defer,\n finalize,\n fromEvent,\n map,\n mapTo,\n mergeMap,\n of,\n shareReplay,\n startWith,\n tap\n} from \"rxjs\"\n\nimport { getElements } from \"~/browser\"\n\nimport { Component } from \"../_\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Palette colors\n */\nexport interface PaletteColor {\n scheme?: string /* Color scheme */\n primary?: string /* Primary color */\n accent?: string /* Accent color */\n}\n\n/**\n * Palette\n */\nexport interface Palette {\n index: number /* Palette index */\n color: PaletteColor /* Palette colors */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch color palette\n *\n * @param inputs - Color palette element\n *\n * @returns Color palette observable\n */\nexport function watchPalette(\n inputs: HTMLInputElement[]\n): Observable {\n const current = __md_get(\"__palette\") || {\n index: inputs.findIndex(input => matchMedia(\n input.getAttribute(\"data-md-color-media\")!\n ).matches)\n }\n\n /* Emit changes in color palette */\n return of(...inputs)\n .pipe(\n mergeMap(input => fromEvent(input, \"change\")\n .pipe(\n mapTo(input)\n )\n ),\n startWith(inputs[Math.max(0, current.index)]),\n map(input => ({\n index: inputs.indexOf(input),\n color: {\n scheme: input.getAttribute(\"data-md-color-scheme\"),\n primary: input.getAttribute(\"data-md-color-primary\"),\n accent: input.getAttribute(\"data-md-color-accent\")\n }\n } as Palette)),\n shareReplay(1)\n )\n}\n\n/**\n * Mount color palette\n *\n * @param el - Color palette element\n *\n * @returns Color palette component observable\n */\nexport function mountPalette(\n el: HTMLElement\n): Observable> {\n return defer(() => {\n const push$ = new Subject()\n push$.subscribe(palette => {\n\n /* Set color palette */\n for (const [key, value] of Object.entries(palette.color))\n document.body.setAttribute(`data-md-color-${key}`, value)\n\n /* Toggle visibility */\n for (let index = 0; index < inputs.length; index++) {\n const label = inputs[index].nextElementSibling\n if (label instanceof HTMLElement)\n label.hidden = palette.index !== index\n }\n\n /* Persist preference in local storage */\n __md_set(\"__palette\", palette)\n })\n\n /* Create and return component */\n const inputs = getElements(\"input\", el)\n return watchPalette(inputs)\n .pipe(\n tap(state => push$.next(state)),\n finalize(() => push$.complete()),\n map(state => ({ ref: el, ...state }))\n )\n })\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport ClipboardJS from \"clipboard\"\nimport {\n Observable,\n Subject,\n mapTo,\n tap\n} from \"rxjs\"\n\nimport { translation } from \"~/_\"\nimport { getElement } from \"~/browser\"\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Setup options\n */\ninterface SetupOptions {\n alert$: Subject /* Alert subject */\n}\n\n/* ----------------------------------------------------------------------------\n * Helper functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Extract text to copy\n *\n * @param el - HTML element\n *\n * @returns Extracted text\n */\nfunction extract(el: HTMLElement): string {\n el.setAttribute(\"data-md-copying\", \"\")\n const text = el.innerText\n el.removeAttribute(\"data-md-copying\")\n return text\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Set up Clipboard.js integration\n *\n * @param options - Options\n */\nexport function setupClipboardJS(\n { alert$ }: SetupOptions\n): void {\n if (ClipboardJS.isSupported()) {\n new Observable(subscriber => {\n new ClipboardJS(\"[data-clipboard-target], [data-clipboard-text]\", {\n text: el => (\n el.getAttribute(\"data-clipboard-text\")! ||\n extract(getElement(\n el.getAttribute(\"data-clipboard-target\")!\n ))\n )\n })\n .on(\"success\", ev => subscriber.next(ev))\n })\n .pipe(\n tap(ev => {\n const trigger = ev.trigger as HTMLElement\n trigger.focus()\n }),\n mapTo(translation(\"clipboard.copied\"))\n )\n .subscribe(alert$)\n }\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n Observable,\n defaultIfEmpty,\n map,\n of,\n tap\n} from \"rxjs\"\n\nimport { configuration } from \"~/_\"\nimport { getElements, requestXML } from \"~/browser\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Sitemap, i.e. a list of URLs\n */\nexport type Sitemap = string[]\n\n/* ----------------------------------------------------------------------------\n * Helper functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Preprocess a list of URLs\n *\n * This function replaces the `site_url` in the sitemap with the actual base\n * URL, to allow instant loading to work in occasions like Netlify previews.\n *\n * @param urls - URLs\n *\n * @returns URL path parts\n */\nfunction preprocess(urls: Sitemap): Sitemap {\n if (urls.length < 2)\n return [\"\"]\n\n /* Take the first two URLs and remove everything after the last slash */\n const [root, next] = [...urls]\n .sort((a, b) => a.length - b.length)\n .map(url => url.replace(/[^/]+$/, \"\"))\n\n /* Compute common prefix */\n let index = 0\n if (root === next)\n index = root.length\n else\n while (root.charCodeAt(index) === next.charCodeAt(index))\n index++\n\n /* Remove common prefix and return in original order */\n return urls.map(url => url.replace(root.slice(0, index), \"\"))\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Fetch the sitemap for the given base URL\n *\n * @param base - Base URL\n *\n * @returns Sitemap observable\n */\nexport function fetchSitemap(base?: URL): Observable {\n const cached = __md_get(\"__sitemap\", sessionStorage, base)\n if (cached) {\n return of(cached)\n } else {\n const config = configuration()\n return requestXML(new URL(\"sitemap.xml\", base || config.base))\n .pipe(\n map(sitemap => preprocess(getElements(\"loc\", sitemap)\n .map(node => node.textContent!)\n )),\n defaultIfEmpty([]),\n tap(sitemap => __md_set(\"__sitemap\", sitemap, sessionStorage, base))\n )\n }\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n EMPTY,\n NEVER,\n Observable,\n Subject,\n bufferCount,\n catchError,\n concatMap,\n debounceTime,\n distinctUntilChanged,\n distinctUntilKeyChanged,\n filter,\n fromEvent,\n map,\n merge,\n of,\n sample,\n share,\n skip,\n skipUntil,\n switchMap\n} from \"rxjs\"\n\nimport { configuration, feature } from \"~/_\"\nimport {\n Viewport,\n ViewportOffset,\n getElements,\n getOptionalElement,\n request,\n setLocation,\n setLocationHash\n} from \"~/browser\"\nimport { getComponentElement } from \"~/components\"\nimport { h } from \"~/utilities\"\n\nimport { fetchSitemap } from \"../sitemap\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * History state\n */\nexport interface HistoryState {\n url: URL /* State URL */\n offset?: ViewportOffset /* State viewport offset */\n}\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Setup options\n */\ninterface SetupOptions {\n document$: Subject /* Document subject */\n location$: Subject /* Location subject */\n viewport$: Observable /* Viewport observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Set up instant loading\n *\n * When fetching, theoretically, we could use `responseType: \"document\"`, but\n * since all MkDocs links are relative, we need to make sure that the current\n * location matches the document we just loaded. Otherwise any relative links\n * in the document could use the old location.\n *\n * This is the reason why we need to synchronize history events and the process\n * of fetching the document for navigation changes (except `popstate` events):\n *\n * 1. Fetch document via `XMLHTTPRequest`\n * 2. Set new location via `history.pushState`\n * 3. Parse and emit fetched document\n *\n * For `popstate` events, we must not use `history.pushState`, or the forward\n * history will be irreversibly overwritten. In case the request fails, the\n * location change is dispatched regularly.\n *\n * @param options - Options\n */\nexport function setupInstantLoading(\n { document$, location$, viewport$ }: SetupOptions\n): void {\n const config = configuration()\n if (location.protocol === \"file:\")\n return\n\n /* Disable automatic scroll restoration */\n if (\"scrollRestoration\" in history) {\n history.scrollRestoration = \"manual\"\n\n /* Hack: ensure that reloads restore viewport offset */\n fromEvent(window, \"beforeunload\")\n .subscribe(() => {\n history.scrollRestoration = \"auto\"\n })\n }\n\n /* Hack: ensure absolute favicon link to omit 404s when switching */\n const favicon = getOptionalElement(\"link[rel=icon]\")\n if (typeof favicon !== \"undefined\")\n favicon.href = favicon.href\n\n /* Intercept internal navigation */\n const push$ = fetchSitemap()\n .pipe(\n map(paths => paths.map(path => `${new URL(path, config.base)}`)),\n switchMap(urls => fromEvent(document.body, \"click\")\n .pipe(\n filter(ev => !ev.metaKey && !ev.ctrlKey),\n switchMap(ev => {\n if (ev.target instanceof Element) {\n const el = ev.target.closest(\"a\")\n if (el && !el.target) {\n const url = new URL(el.href)\n\n /* Canonicalize URL */\n url.search = \"\"\n url.hash = \"\"\n\n /* Check if URL should be intercepted */\n if (\n url.pathname !== location.pathname &&\n urls.includes(url.toString())\n ) {\n ev.preventDefault()\n return of({\n url: new URL(el.href)\n })\n }\n }\n }\n return NEVER\n })\n )\n ),\n share()\n )\n\n /* Intercept history back and forward */\n const pop$ = fromEvent(window, \"popstate\")\n .pipe(\n filter(ev => ev.state !== null),\n map(ev => ({\n url: new URL(location.href),\n offset: ev.state\n })),\n share()\n )\n\n /* Emit location change */\n merge(push$, pop$)\n .pipe(\n distinctUntilChanged((a, b) => a.url.href === b.url.href),\n map(({ url }) => url)\n )\n .subscribe(location$)\n\n /* Fetch document via `XMLHTTPRequest` */\n const response$ = location$\n .pipe(\n distinctUntilKeyChanged(\"pathname\"),\n switchMap(url => request(url.href)\n .pipe(\n catchError(() => {\n setLocation(url)\n return NEVER\n })\n )\n ),\n share()\n )\n\n /* Set new location via `history.pushState` */\n push$\n .pipe(\n sample(response$)\n )\n .subscribe(({ url }) => {\n history.pushState({}, \"\", `${url}`)\n })\n\n /* Parse and emit fetched document */\n const dom = new DOMParser()\n response$\n .pipe(\n switchMap(res => res.text()),\n map(res => dom.parseFromString(res, \"text/html\"))\n )\n .subscribe(document$)\n\n /* Replace meta tags and components */\n document$\n .pipe(\n skip(1)\n )\n .subscribe(replacement => {\n for (const selector of [\n\n /* Meta tags */\n \"title\",\n \"link[rel=canonical]\",\n \"meta[name=author]\",\n \"meta[name=description]\",\n\n /* Components */\n \"[data-md-component=announce]\",\n \"[data-md-component=container]\",\n \"[data-md-component=header-topic]\",\n \"[data-md-component=outdated]\",\n \"[data-md-component=logo]\",\n \"[data-md-component=skip]\",\n ...feature(\"navigation.tabs.sticky\")\n ? [\"[data-md-component=tabs]\"]\n : []\n ]) {\n const source = getOptionalElement(selector)\n const target = getOptionalElement(selector, replacement)\n if (\n typeof source !== \"undefined\" &&\n typeof target !== \"undefined\"\n ) {\n source.replaceWith(target)\n }\n }\n })\n\n /* Re-evaluate scripts */\n document$\n .pipe(\n skip(1),\n map(() => getComponentElement(\"container\")),\n switchMap(el => getElements(\"script\", el)),\n concatMap(el => {\n const script = h(\"script\")\n if (el.src) {\n for (const name of el.getAttributeNames())\n script.setAttribute(name, el.getAttribute(name)!)\n el.replaceWith(script)\n\n /* Complete when script is loaded */\n return new Observable(observer => {\n script.onload = () => observer.complete()\n })\n\n /* Complete immediately */\n } else {\n script.textContent = el.textContent\n el.replaceWith(script)\n return EMPTY\n }\n })\n )\n .subscribe()\n\n /* Emit history state change */\n merge(push$, pop$)\n .pipe(\n sample(document$)\n )\n .subscribe(({ url, offset }) => {\n if (url.hash && !offset) {\n setLocationHash(url.hash)\n } else {\n window.scrollTo(0, offset?.y || 0)\n }\n })\n\n /* Debounce update of viewport offset */\n viewport$\n .pipe(\n skipUntil(push$),\n debounceTime(250),\n distinctUntilKeyChanged(\"offset\")\n )\n .subscribe(({ offset }) => {\n history.replaceState(offset, \"\")\n })\n\n /* Set viewport offset from history */\n merge(push$, pop$)\n .pipe(\n bufferCount(2, 1),\n filter(([a, b]) => a.url.pathname === b.url.pathname),\n map(([, state]) => state)\n )\n .subscribe(({ offset }) => {\n window.scrollTo(0, offset?.y || 0)\n })\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport escapeHTML from \"escape-html\"\n\nimport { SearchIndexDocument } from \"../_\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Search document\n */\nexport interface SearchDocument extends SearchIndexDocument {\n parent?: SearchIndexDocument /* Parent article */\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Search document mapping\n */\nexport type SearchDocumentMap = Map\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Create a search document mapping\n *\n * @param docs - Search index documents\n *\n * @returns Search document map\n */\nexport function setupSearchDocumentMap(\n docs: SearchIndexDocument[]\n): SearchDocumentMap {\n const documents = new Map()\n const parents = new Set()\n for (const doc of docs) {\n const [path, hash] = doc.location.split(\"#\")\n\n /* Extract location, title and tags */\n const location = doc.location\n const title = doc.title\n const tags = doc.tags\n\n /* Escape and cleanup text */\n const text = escapeHTML(doc.text)\n .replace(/\\s+(?=[,.:;!?])/g, \"\")\n .replace(/\\s+/g, \" \")\n\n /* Handle section */\n if (hash) {\n const parent = documents.get(path)!\n\n /* Ignore first section, override article */\n if (!parents.has(parent)) {\n parent.title = doc.title\n parent.text = text\n\n /* Remember that we processed the article */\n parents.add(parent)\n\n /* Add subsequent section */\n } else {\n documents.set(location, {\n location,\n title,\n text,\n parent\n })\n }\n\n /* Add article */\n } else {\n documents.set(location, {\n location,\n title,\n text,\n ...tags && { tags }\n })\n }\n }\n return documents\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport escapeHTML from \"escape-html\"\n\nimport { SearchIndexConfig } from \"../_\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Search highlight function\n *\n * @param value - Value\n *\n * @returns Highlighted value\n */\nexport type SearchHighlightFn = (value: string) => string\n\n/**\n * Search highlight factory function\n *\n * @param query - Query value\n *\n * @returns Search highlight function\n */\nexport type SearchHighlightFactoryFn = (query: string) => SearchHighlightFn\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Create a search highlighter\n *\n * @param config - Search index configuration\n * @param escape - Whether to escape HTML\n *\n * @returns Search highlight factory function\n */\nexport function setupSearchHighlighter(\n config: SearchIndexConfig, escape: boolean\n): SearchHighlightFactoryFn {\n const separator = new RegExp(config.separator, \"img\")\n const highlight = (_: unknown, data: string, term: string) => {\n return `${data}${term}`\n }\n\n /* Return factory function */\n return (query: string) => {\n query = query\n .replace(/[\\s*+\\-:~^]+/g, \" \")\n .trim()\n\n /* Create search term match expression */\n const match = new RegExp(`(^|${config.separator})(${\n query\n .replace(/[|\\\\{}()[\\]^$+*?.-]/g, \"\\\\$&\")\n .replace(separator, \"|\")\n })`, \"img\")\n\n /* Highlight string value */\n return value => (\n escape\n ? escapeHTML(value)\n : value\n )\n .replace(match, highlight)\n .replace(/<\\/mark>(\\s+)]*>/img, \"$1\")\n }\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Search transformation function\n *\n * @param value - Query value\n *\n * @returns Transformed query value\n */\nexport type SearchTransformFn = (value: string) => string\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Default transformation function\n *\n * 1. Search for terms in quotation marks and prepend a `+` modifier to denote\n * that the resulting document must contain all terms, converting the query\n * to an `AND` query (as opposed to the default `OR` behavior). While users\n * may expect terms enclosed in quotation marks to map to span queries, i.e.\n * for which order is important, Lunr.js doesn't support them, so the best\n * we can do is to convert the terms to an `AND` query.\n *\n * 2. Replace control characters which are not located at the beginning of the\n * query or preceded by white space, or are not followed by a non-whitespace\n * character or are at the end of the query string. Furthermore, filter\n * unmatched quotation marks.\n *\n * 3. Trim excess whitespace from left and right.\n *\n * @param query - Query value\n *\n * @returns Transformed query value\n */\nexport function defaultTransform(query: string): string {\n return query\n .split(/\"([^\"]+)\"/g) /* => 1 */\n .map((terms, index) => index & 1\n ? terms.replace(/^\\b|^(?![^\\x00-\\x7F]|$)|\\s+/g, \" +\")\n : terms\n )\n .join(\"\")\n .replace(/\"|(?:^|\\s+)[*+\\-:^~]+(?=\\s+|$)/g, \"\") /* => 2 */\n .trim() /* => 3 */\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A RTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { SearchIndex, SearchResult } from \"../../_\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Search message type\n */\nexport const enum SearchMessageType {\n SETUP, /* Search index setup */\n READY, /* Search index ready */\n QUERY, /* Search query */\n RESULT /* Search results */\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Message containing the data necessary to setup the search index\n */\nexport interface SearchSetupMessage {\n type: SearchMessageType.SETUP /* Message type */\n data: SearchIndex /* Message data */\n}\n\n/**\n * Message indicating the search index is ready\n */\nexport interface SearchReadyMessage {\n type: SearchMessageType.READY /* Message type */\n}\n\n/**\n * Message containing a search query\n */\nexport interface SearchQueryMessage {\n type: SearchMessageType.QUERY /* Message type */\n data: string /* Message data */\n}\n\n/**\n * Message containing results for a search query\n */\nexport interface SearchResultMessage {\n type: SearchMessageType.RESULT /* Message type */\n data: SearchResult /* Message data */\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Message exchanged with the search worker\n */\nexport type SearchMessage =\n | SearchSetupMessage\n | SearchReadyMessage\n | SearchQueryMessage\n | SearchResultMessage\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Type guard for search setup messages\n *\n * @param message - Search worker message\n *\n * @returns Test result\n */\nexport function isSearchSetupMessage(\n message: SearchMessage\n): message is SearchSetupMessage {\n return message.type === SearchMessageType.SETUP\n}\n\n/**\n * Type guard for search ready messages\n *\n * @param message - Search worker message\n *\n * @returns Test result\n */\nexport function isSearchReadyMessage(\n message: SearchMessage\n): message is SearchReadyMessage {\n return message.type === SearchMessageType.READY\n}\n\n/**\n * Type guard for search query messages\n *\n * @param message - Search worker message\n *\n * @returns Test result\n */\nexport function isSearchQueryMessage(\n message: SearchMessage\n): message is SearchQueryMessage {\n return message.type === SearchMessageType.QUERY\n}\n\n/**\n * Type guard for search result messages\n *\n * @param message - Search worker message\n *\n * @returns Test result\n */\nexport function isSearchResultMessage(\n message: SearchMessage\n): message is SearchResultMessage {\n return message.type === SearchMessageType.RESULT\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A RTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n ObservableInput,\n Subject,\n from,\n map,\n share\n} from \"rxjs\"\n\nimport { configuration, feature, translation } from \"~/_\"\nimport { WorkerHandler, watchWorker } from \"~/browser\"\n\nimport { SearchIndex } from \"../../_\"\nimport {\n SearchOptions,\n SearchPipeline\n} from \"../../options\"\nimport {\n SearchMessage,\n SearchMessageType,\n SearchSetupMessage,\n isSearchResultMessage\n} from \"../message\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Search worker\n */\nexport type SearchWorker = WorkerHandler\n\n/* ----------------------------------------------------------------------------\n * Helper functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Set up search index\n *\n * @param data - Search index\n *\n * @returns Search index\n */\nfunction setupSearchIndex({ config, docs }: SearchIndex): SearchIndex {\n\n /* Override default language with value from translation */\n if (config.lang.length === 1 && config.lang[0] === \"en\")\n config.lang = [\n translation(\"search.config.lang\")\n ]\n\n /* Override default separator with value from translation */\n if (config.separator === \"[\\\\s\\\\-]+\")\n config.separator = translation(\"search.config.separator\")\n\n /* Set pipeline from translation */\n const pipeline = translation(\"search.config.pipeline\")\n .split(/\\s*,\\s*/)\n .filter(Boolean) as SearchPipeline\n\n /* Determine search options */\n const options: SearchOptions = {\n pipeline,\n suggestions: feature(\"search.suggest\")\n }\n\n /* Return search index after defaulting */\n return { config, docs, options }\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Set up search worker\n *\n * This function creates a web worker to set up and query the search index,\n * which is done using Lunr.js. The index must be passed as an observable to\n * enable hacks like _localsearch_ via search index embedding as JSON.\n *\n * @param url - Worker URL\n * @param index - Search index observable input\n *\n * @returns Search worker\n */\nexport function setupSearchWorker(\n url: string, index: ObservableInput\n): SearchWorker {\n const config = configuration()\n const worker = new Worker(url)\n\n /* Create communication channels and resolve relative links */\n const tx$ = new Subject()\n const rx$ = watchWorker(worker, { tx$ })\n .pipe(\n map(message => {\n if (isSearchResultMessage(message)) {\n for (const result of message.data.items)\n for (const document of result)\n document.location = `${new URL(document.location, config.base)}`\n }\n return message\n }),\n share()\n )\n\n /* Set up search index */\n from(index)\n .pipe(\n map(data => ({\n type: SearchMessageType.SETUP,\n data: setupSearchIndex(data)\n } as SearchSetupMessage))\n )\n .subscribe(tx$.next.bind(tx$))\n\n /* Return search worker */\n return { tx$, rx$ }\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n EMPTY,\n Subject,\n combineLatest,\n filter,\n fromEvent,\n map,\n of,\n switchMap,\n switchMapTo\n} from \"rxjs\"\n\nimport { configuration } from \"~/_\"\nimport {\n getElement,\n getLocation,\n requestJSON,\n setLocation\n} from \"~/browser\"\nimport { getComponentElements } from \"~/components\"\nimport {\n Version,\n renderVersionSelector\n} from \"~/templates\"\n\nimport { fetchSitemap } from \"../sitemap\"\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Setup options\n */\ninterface SetupOptions {\n document$: Subject /* Document subject */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Set up version selector\n *\n * @param options - Options\n */\nexport function setupVersionSelector(\n { document$ }: SetupOptions\n): void {\n const config = configuration()\n const versions$ = requestJSON(\n new URL(\"../versions.json\", config.base)\n )\n\n /* Determine current version */\n const current$ = versions$\n .pipe(\n map(versions => {\n const [, current] = config.base.match(/([^/]+)\\/?$/)!\n return versions.find(({ version, aliases }) => (\n version === current || aliases.includes(current)\n )) || versions[0]\n })\n )\n\n /* Intercept inter-version navigation */\n combineLatest([versions$, current$])\n .pipe(\n map(([versions, current]) => new Map(versions\n .filter(version => version !== current)\n .map(version => [\n `${new URL(`../${version.version}/`, config.base)}`,\n version\n ])\n )),\n switchMap(urls => fromEvent(document.body, \"click\")\n .pipe(\n filter(ev => !ev.metaKey && !ev.ctrlKey),\n switchMap(ev => {\n if (ev.target instanceof Element) {\n const el = ev.target.closest(\"a\")\n if (el && !el.target && urls.has(el.href)) {\n ev.preventDefault()\n return of(el.href)\n }\n }\n return EMPTY\n }),\n switchMap(url => {\n const { version } = urls.get(url)!\n return fetchSitemap(new URL(url))\n .pipe(\n map(sitemap => {\n const location = getLocation()\n const path = location.href.replace(config.base, \"\")\n return sitemap.includes(path)\n ? new URL(`../${version}/${path}`, config.base)\n : new URL(url)\n })\n )\n })\n )\n )\n )\n .subscribe(url => setLocation(url))\n\n /* Render version selector and warning */\n combineLatest([versions$, current$])\n .subscribe(([versions, current]) => {\n const topic = getElement(\".md-header__topic\")\n topic.appendChild(renderVersionSelector(versions, current))\n })\n\n /* Integrate outdated version banner with instant loading */\n document$.pipe(switchMapTo(current$))\n .subscribe(current => {\n\n /* Check if version state was already determined */\n let outdated = __md_get(\"__outdated\", sessionStorage)\n if (outdated === null) {\n const latest = config.version?.default || \"latest\"\n outdated = !current.aliases.includes(latest)\n\n /* Persist version state in session storage */\n __md_set(\"__outdated\", outdated, sessionStorage)\n }\n\n /* Unhide outdated version banner */\n if (outdated)\n for (const warning of getComponentElements(\"outdated\"))\n warning.hidden = false\n })\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n Observable,\n Subject,\n combineLatest,\n delay,\n distinctUntilChanged,\n distinctUntilKeyChanged,\n filter,\n finalize,\n fromEvent,\n map,\n merge,\n shareReplay,\n startWith,\n take,\n takeLast,\n takeUntil,\n tap\n} from \"rxjs\"\n\nimport { translation } from \"~/_\"\nimport {\n getLocation,\n setToggle,\n watchElementFocus,\n watchToggle\n} from \"~/browser\"\nimport {\n SearchMessageType,\n SearchQueryMessage,\n SearchWorker,\n defaultTransform,\n isSearchReadyMessage\n} from \"~/integrations\"\n\nimport { Component } from \"../../_\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Search query\n */\nexport interface SearchQuery {\n value: string /* Query value */\n focus: boolean /* Query focus */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch search query\n *\n * Note that the focus event which triggers re-reading the current query value\n * is delayed by `1ms` so the input's empty state is allowed to propagate.\n *\n * @param el - Search query element\n * @param worker - Search worker\n *\n * @returns Search query observable\n */\nexport function watchSearchQuery(\n el: HTMLInputElement, { rx$ }: SearchWorker\n): Observable {\n const fn = __search?.transform || defaultTransform\n\n /* Immediately show search dialog */\n const { searchParams } = getLocation()\n if (searchParams.has(\"q\"))\n setToggle(\"search\", true)\n\n /* Intercept query parameter (deep link) */\n const param$ = rx$\n .pipe(\n filter(isSearchReadyMessage),\n take(1),\n map(() => searchParams.get(\"q\") || \"\")\n )\n\n /* Remove query parameter when search is closed */\n watchToggle(\"search\")\n .pipe(\n filter(active => !active),\n take(1)\n )\n .subscribe(() => {\n const url = new URL(location.href)\n url.searchParams.delete(\"q\")\n history.replaceState({}, \"\", `${url}`)\n })\n\n /* Set query from parameter */\n param$.subscribe(value => { // TODO: not ideal - find a better way\n if (value)\n el.value = value\n })\n\n /* Intercept focus and input events */\n const focus$ = watchElementFocus(el)\n const value$ = merge(\n fromEvent(el, \"keyup\"),\n fromEvent(el, \"focus\").pipe(delay(1)),\n param$\n )\n .pipe(\n map(() => fn(el.value)),\n startWith(\"\"),\n distinctUntilChanged(),\n )\n\n /* Combine into single observable */\n return combineLatest([value$, focus$])\n .pipe(\n map(([value, focus]) => ({ value, focus })),\n shareReplay(1)\n )\n}\n\n/**\n * Mount search query\n *\n * @param el - Search query element\n * @param worker - Search worker\n *\n * @returns Search query component observable\n */\nexport function mountSearchQuery(\n el: HTMLInputElement, { tx$, rx$ }: SearchWorker\n): Observable> {\n const push$ = new Subject()\n\n /* Handle value changes */\n push$\n .pipe(\n distinctUntilKeyChanged(\"value\"),\n map(({ value }): SearchQueryMessage => ({\n type: SearchMessageType.QUERY,\n data: value\n }))\n )\n .subscribe(tx$.next.bind(tx$))\n\n /* Handle focus changes */\n push$\n .pipe(\n distinctUntilKeyChanged(\"focus\")\n )\n .subscribe(({ focus }) => {\n if (focus) {\n setToggle(\"search\", focus)\n el.placeholder = \"\"\n } else {\n el.placeholder = translation(\"search.placeholder\")\n }\n })\n\n /* Handle reset */\n fromEvent(el.form!, \"reset\")\n .pipe(\n takeUntil(push$.pipe(takeLast(1)))\n )\n .subscribe(() => el.focus())\n\n /* Create and return component */\n return watchSearchQuery(el, { tx$, rx$ })\n .pipe(\n tap(state => push$.next(state)),\n finalize(() => push$.complete()),\n map(state => ({ ref: el, ...state }))\n )\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n Observable,\n Subject,\n bufferCount,\n filter,\n finalize,\n map,\n merge,\n of,\n skipUntil,\n switchMap,\n take,\n tap,\n withLatestFrom,\n zipWith\n} from \"rxjs\"\n\nimport { translation } from \"~/_\"\nimport {\n getElement,\n watchElementBoundary\n} from \"~/browser\"\nimport {\n SearchResult,\n SearchWorker,\n isSearchReadyMessage,\n isSearchResultMessage\n} from \"~/integrations\"\nimport { renderSearchResultItem } from \"~/templates\"\nimport { round } from \"~/utilities\"\n\nimport { Component } from \"../../_\"\nimport { SearchQuery } from \"../query\"\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Mount options\n */\ninterface MountOptions {\n query$: Observable /* Search query observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Mount search result list\n *\n * This function performs a lazy rendering of the search results, depending on\n * the vertical offset of the search result container.\n *\n * @param el - Search result list element\n * @param worker - Search worker\n * @param options - Options\n *\n * @returns Search result list component observable\n */\nexport function mountSearchResult(\n el: HTMLElement, { rx$ }: SearchWorker, { query$ }: MountOptions\n): Observable> {\n const push$ = new Subject()\n const boundary$ = watchElementBoundary(el.parentElement!)\n .pipe(\n filter(Boolean)\n )\n\n /* Retrieve nested components */\n const meta = getElement(\":scope > :first-child\", el)\n const list = getElement(\":scope > :last-child\", el)\n\n /* Wait until search is ready */\n const ready$ = rx$\n .pipe(\n filter(isSearchReadyMessage),\n take(1)\n )\n\n /* Update search result metadata */\n push$\n .pipe(\n withLatestFrom(query$),\n skipUntil(ready$)\n )\n .subscribe(([{ items }, { value }]) => {\n if (value) {\n switch (items.length) {\n\n /* No results */\n case 0:\n meta.textContent = translation(\"search.result.none\")\n break\n\n /* One result */\n case 1:\n meta.textContent = translation(\"search.result.one\")\n break\n\n /* Multiple result */\n default:\n meta.textContent = translation(\n \"search.result.other\",\n round(items.length)\n )\n }\n } else {\n meta.textContent = translation(\"search.result.placeholder\")\n }\n })\n\n /* Update search result list */\n push$\n .pipe(\n tap(() => list.innerHTML = \"\"),\n switchMap(({ items }) => merge(\n of(...items.slice(0, 10)),\n of(...items.slice(10))\n .pipe(\n bufferCount(4),\n zipWith(boundary$),\n switchMap(([chunk]) => chunk)\n )\n ))\n )\n .subscribe(result => list.appendChild(\n renderSearchResultItem(result)\n ))\n\n /* Filter search result message */\n const result$ = rx$\n .pipe(\n filter(isSearchResultMessage),\n map(({ data }) => data)\n )\n\n /* Create and return component */\n return result$\n .pipe(\n tap(state => push$.next(state)),\n finalize(() => push$.complete()),\n map(state => ({ ref: el, ...state }))\n )\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n Observable,\n Subject,\n finalize,\n fromEvent,\n map,\n tap\n} from \"rxjs\"\n\nimport { getLocation } from \"~/browser\"\n\nimport { Component } from \"../../_\"\nimport { SearchQuery } from \"../query\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Search sharing\n */\nexport interface SearchShare {\n url: URL /* Deep link for sharing */\n}\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch options\n */\ninterface WatchOptions {\n query$: Observable /* Search query observable */\n}\n\n/**\n * Mount options\n */\ninterface MountOptions {\n query$: Observable /* Search query observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Mount search sharing\n *\n * @param _el - Search sharing element\n * @param options - Options\n *\n * @returns Search sharing observable\n */\nexport function watchSearchShare(\n _el: HTMLElement, { query$ }: WatchOptions\n): Observable {\n return query$\n .pipe(\n map(({ value }) => {\n const url = getLocation()\n url.hash = \"\"\n url.searchParams.delete(\"h\")\n url.searchParams.set(\"q\", value)\n return { url }\n })\n )\n}\n\n/**\n * Mount search sharing\n *\n * @param el - Search sharing element\n * @param options - Options\n *\n * @returns Search sharing component observable\n */\nexport function mountSearchShare(\n el: HTMLAnchorElement, options: MountOptions\n): Observable> {\n const push$ = new Subject()\n push$.subscribe(({ url }) => {\n el.setAttribute(\"data-clipboard-text\", el.href)\n el.href = `${url}`\n })\n\n /* Prevent following of link */\n fromEvent(el, \"click\")\n .subscribe(ev => ev.preventDefault())\n\n /* Create and return component */\n return watchSearchShare(el, options)\n .pipe(\n tap(state => push$.next(state)),\n finalize(() => push$.complete()),\n map(state => ({ ref: el, ...state }))\n )\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n Observable,\n Subject,\n asyncScheduler,\n combineLatestWith,\n distinctUntilChanged,\n filter,\n finalize,\n fromEvent,\n map,\n merge,\n observeOn,\n tap\n} from \"rxjs\"\n\nimport { Keyboard } from \"~/browser\"\nimport {\n SearchResult,\n SearchWorker,\n isSearchResultMessage\n} from \"~/integrations\"\n\nimport { Component, getComponentElement } from \"../../_\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Search suggestions\n */\nexport interface SearchSuggest {}\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Mount options\n */\ninterface MountOptions {\n keyboard$: Observable /* Keyboard observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Mount search suggestions\n *\n * This function will perform a lazy rendering of the search results, depending\n * on the vertical offset of the search result container.\n *\n * @param el - Search result list element\n * @param worker - Search worker\n * @param options - Options\n *\n * @returns Search result list component observable\n */\nexport function mountSearchSuggest(\n el: HTMLElement, { rx$ }: SearchWorker, { keyboard$ }: MountOptions\n): Observable> {\n const push$ = new Subject()\n\n /* Retrieve query component and track all changes */\n const query = getComponentElement(\"search-query\")\n const query$ = merge(\n fromEvent(query, \"keydown\"),\n fromEvent(query, \"focus\")\n )\n .pipe(\n observeOn(asyncScheduler),\n map(() => query.value),\n distinctUntilChanged(),\n )\n\n /* Update search suggestions */\n push$\n .pipe(\n combineLatestWith(query$),\n map(([{ suggestions }, value]) => {\n const words = value.split(/([\\s-]+)/)\n if (suggestions?.length && words[words.length - 1]) {\n const last = suggestions[suggestions.length - 1]\n if (last.startsWith(words[words.length - 1]))\n words[words.length - 1] = last\n } else {\n words.length = 0\n }\n return words\n })\n )\n .subscribe(words => el.innerHTML = words\n .join(\"\")\n .replace(/\\s/g, \" \")\n )\n\n /* Set up search keyboard handlers */\n keyboard$\n .pipe(\n filter(({ mode }) => mode === \"search\")\n )\n .subscribe(key => {\n switch (key.type) {\n\n /* Right arrow: accept current suggestion */\n case \"ArrowRight\":\n if (\n el.innerText.length &&\n query.selectionStart === query.value.length\n )\n query.value = el.innerText\n break\n }\n })\n\n /* Filter search result message */\n const result$ = rx$\n .pipe(\n filter(isSearchResultMessage),\n map(({ data }) => data)\n )\n\n /* Create and return component */\n return result$\n .pipe(\n tap(state => push$.next(state)),\n finalize(() => push$.complete()),\n map(() => ({ ref: el }))\n )\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n NEVER,\n Observable,\n ObservableInput,\n filter,\n merge,\n mergeWith,\n sample,\n take\n} from \"rxjs\"\n\nimport { configuration } from \"~/_\"\nimport {\n Keyboard,\n getActiveElement,\n getElements,\n setToggle\n} from \"~/browser\"\nimport {\n SearchIndex,\n SearchResult,\n isSearchQueryMessage,\n isSearchReadyMessage,\n setupSearchWorker\n} from \"~/integrations\"\n\nimport {\n Component,\n getComponentElement,\n getComponentElements\n} from \"../../_\"\nimport {\n SearchQuery,\n mountSearchQuery\n} from \"../query\"\nimport { mountSearchResult } from \"../result\"\nimport {\n SearchShare,\n mountSearchShare\n} from \"../share\"\nimport {\n SearchSuggest,\n mountSearchSuggest\n} from \"../suggest\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Search\n */\nexport type Search =\n | SearchQuery\n | SearchResult\n | SearchShare\n | SearchSuggest\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Mount options\n */\ninterface MountOptions {\n index$: ObservableInput /* Search index observable */\n keyboard$: Observable /* Keyboard observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Mount search\n *\n * This function sets up the search functionality, including the underlying\n * web worker and all keyboard bindings.\n *\n * @param el - Search element\n * @param options - Options\n *\n * @returns Search component observable\n */\nexport function mountSearch(\n el: HTMLElement, { index$, keyboard$ }: MountOptions\n): Observable> {\n const config = configuration()\n try {\n const url = __search?.worker || config.search\n const worker = setupSearchWorker(url, index$)\n\n /* Retrieve query and result components */\n const query = getComponentElement(\"search-query\", el)\n const result = getComponentElement(\"search-result\", el)\n\n /* Re-emit query when search is ready */\n const { tx$, rx$ } = worker\n tx$\n .pipe(\n filter(isSearchQueryMessage),\n sample(rx$.pipe(filter(isSearchReadyMessage))),\n take(1)\n )\n .subscribe(tx$.next.bind(tx$))\n\n /* Set up search keyboard handlers */\n keyboard$\n .pipe(\n filter(({ mode }) => mode === \"search\")\n )\n .subscribe(key => {\n const active = getActiveElement()\n switch (key.type) {\n\n /* Enter: go to first (best) result */\n case \"Enter\":\n if (active === query) {\n const anchors = new Map()\n for (const anchor of getElements(\n \":first-child [href]\", result\n )) {\n const article = anchor.firstElementChild!\n anchors.set(anchor, parseFloat(\n article.getAttribute(\"data-md-score\")!\n ))\n }\n\n /* Go to result with highest score, if any */\n if (anchors.size) {\n const [[best]] = [...anchors].sort(([, a], [, b]) => b - a)\n best.click()\n }\n\n /* Otherwise omit form submission */\n key.claim()\n }\n break\n\n /* Escape or Tab: close search */\n case \"Escape\":\n case \"Tab\":\n setToggle(\"search\", false)\n query.blur()\n break\n\n /* Vertical arrows: select previous or next search result */\n case \"ArrowUp\":\n case \"ArrowDown\":\n if (typeof active === \"undefined\") {\n query.focus()\n } else {\n const els = [query, ...getElements(\n \":not(details) > [href], summary, details[open] [href]\",\n result\n )]\n const i = Math.max(0, (\n Math.max(0, els.indexOf(active)) + els.length + (\n key.type === \"ArrowUp\" ? -1 : +1\n )\n ) % els.length)\n els[i].focus()\n }\n\n /* Prevent scrolling of page */\n key.claim()\n break\n\n /* All other keys: hand to search query */\n default:\n if (query !== getActiveElement())\n query.focus()\n }\n })\n\n /* Set up global keyboard handlers */\n keyboard$\n .pipe(\n filter(({ mode }) => mode === \"global\"),\n )\n .subscribe(key => {\n switch (key.type) {\n\n /* Open search and select query */\n case \"f\":\n case \"s\":\n case \"/\":\n query.focus()\n query.select()\n\n /* Prevent scrolling of page */\n key.claim()\n break\n }\n })\n\n /* Create and return component */\n const query$ = mountSearchQuery(query, worker)\n const result$ = mountSearchResult(result, worker, { query$ })\n return merge(query$, result$)\n .pipe(\n mergeWith(\n\n /* Search sharing */\n ...getComponentElements(\"search-share\", el)\n .map(child => mountSearchShare(child, { query$ })),\n\n /* Search suggestions */\n ...getComponentElements(\"search-suggest\", el)\n .map(child => mountSearchSuggest(child, worker, { keyboard$ }))\n )\n )\n\n /* Gracefully handle broken search */\n } catch (err) {\n el.hidden = true\n return NEVER\n }\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n Observable,\n ObservableInput,\n combineLatest,\n filter,\n map,\n startWith\n} from \"rxjs\"\n\nimport { getLocation } from \"~/browser\"\nimport {\n SearchIndex,\n setupSearchHighlighter\n} from \"~/integrations\"\nimport { h } from \"~/utilities\"\n\nimport { Component } from \"../../_\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Search highlighting\n */\nexport interface SearchHighlight {\n nodes: Map /* Map of replacements */\n}\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Mount options\n */\ninterface MountOptions {\n index$: ObservableInput /* Search index observable */\n location$: Observable /* Location observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Mount search highlighting\n *\n * @param el - Content element\n * @param options - Options\n *\n * @returns Search highlighting component observable\n */\nexport function mountSearchHiglight(\n el: HTMLElement, { index$, location$ }: MountOptions\n): Observable> {\n return combineLatest([\n index$,\n location$\n .pipe(\n startWith(getLocation()),\n filter(url => !!url.searchParams.get(\"h\"))\n )\n ])\n .pipe(\n map(([index, url]) => setupSearchHighlighter(index.config, true)(\n url.searchParams.get(\"h\")!\n )),\n map(fn => {\n const nodes = new Map()\n\n /* Traverse text nodes and collect matches */\n const it = document.createNodeIterator(el, NodeFilter.SHOW_TEXT)\n for (let node = it.nextNode(); node; node = it.nextNode()) {\n if (node.parentElement?.offsetHeight) {\n const original = node.textContent!\n const replaced = fn(original)\n if (replaced.length > original.length)\n nodes.set(node as ChildNode, replaced)\n }\n }\n\n /* Replace original nodes with matches */\n for (const [node, text] of nodes) {\n const { childNodes } = h(\"span\", null, text)\n node.replaceWith(...Array.from(childNodes))\n }\n\n /* Return component */\n return { ref: el, nodes }\n })\n )\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n Observable,\n Subject,\n animationFrameScheduler,\n auditTime,\n combineLatest,\n defer,\n distinctUntilChanged,\n finalize,\n map,\n tap,\n withLatestFrom\n} from \"rxjs\"\n\nimport {\n Viewport,\n getElement,\n getElementOffset\n} from \"~/browser\"\n\nimport { Component } from \"../_\"\nimport { Header } from \"../header\"\nimport { Main } from \"../main\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Sidebar\n */\nexport interface Sidebar {\n height: number /* Sidebar height */\n locked: boolean /* Sidebar is locked */\n}\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch options\n */\ninterface WatchOptions {\n viewport$: Observable /* Viewport observable */\n main$: Observable
    /* Main area observable */\n}\n\n/**\n * Mount options\n */\ninterface MountOptions {\n viewport$: Observable /* Viewport observable */\n header$: Observable
    /* Header observable */\n main$: Observable
    /* Main area observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch sidebar\n *\n * This function returns an observable that computes the visual parameters of\n * the sidebar which depends on the vertical viewport offset, as well as the\n * height of the main area. When the page is scrolled beyond the header, the\n * sidebar is locked and fills the remaining space.\n *\n * @param el - Sidebar element\n * @param options - Options\n *\n * @returns Sidebar observable\n */\nexport function watchSidebar(\n el: HTMLElement, { viewport$, main$ }: WatchOptions\n): Observable {\n const parent = el.parentElement!\n const adjust =\n parent.offsetTop -\n parent.parentElement!.offsetTop\n\n /* Compute the sidebar's available height and if it should be locked */\n return combineLatest([main$, viewport$])\n .pipe(\n map(([{ offset, height }, { offset: { y } }]) => {\n height = height\n + Math.min(adjust, Math.max(0, y - offset))\n - adjust\n return {\n height,\n locked: y >= offset + adjust\n }\n }),\n distinctUntilChanged((a, b) => (\n a.height === b.height &&\n a.locked === b.locked\n ))\n )\n}\n\n/**\n * Mount sidebar\n *\n * This function doesn't set the height of the actual sidebar, but of its first\n * child \u2013 the `.md-sidebar__scrollwrap` element in order to mitigiate jittery\n * sidebars when the footer is scrolled into view. At some point we switched\n * from `absolute` / `fixed` positioning to `sticky` positioning, significantly\n * reducing jitter in some browsers (respectively Firefox and Safari) when\n * scrolling from the top. However, top-aligned sticky positioning means that\n * the sidebar snaps to the bottom when the end of the container is reached.\n * This is what leads to the mentioned jitter, as the sidebar's height may be\n * updated too slowly.\n *\n * This behaviour can be mitigiated by setting the height of the sidebar to `0`\n * while preserving the padding, and the height on its first element.\n *\n * @param el - Sidebar element\n * @param options - Options\n *\n * @returns Sidebar component observable\n */\nexport function mountSidebar(\n el: HTMLElement, { header$, ...options }: MountOptions\n): Observable> {\n const inner = getElement(\".md-sidebar__scrollwrap\", el)\n const { y } = getElementOffset(inner)\n return defer(() => {\n const push$ = new Subject()\n push$\n .pipe(\n auditTime(0, animationFrameScheduler),\n withLatestFrom(header$)\n )\n .subscribe({\n\n /* Handle emission */\n next([{ height }, { height: offset }]) {\n inner.style.height = `${height - 2 * y}px`\n el.style.top = `${offset}px`\n },\n\n /* Handle complete */\n complete() {\n inner.style.height = \"\"\n el.style.top = \"\"\n }\n })\n\n /* Create and return component */\n return watchSidebar(el, options)\n .pipe(\n tap(state => push$.next(state)),\n finalize(() => push$.complete()),\n map(state => ({ ref: el, ...state }))\n )\n })\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { Repo, User } from \"github-types\"\nimport {\n Observable,\n defaultIfEmpty,\n map,\n zip\n} from \"rxjs\"\n\nimport { requestJSON } from \"~/browser\"\n\nimport { SourceFacts } from \"../_\"\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * GitHub release (partial)\n */\ninterface Release {\n tag_name: string /* Tag name */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Fetch GitHub repository facts\n *\n * @param user - GitHub user or organization\n * @param repo - GitHub repository\n *\n * @returns Repository facts observable\n */\nexport function fetchSourceFactsFromGitHub(\n user: string, repo?: string\n): Observable {\n if (typeof repo !== \"undefined\") {\n const url = `https://api.github.com/repos/${user}/${repo}`\n return zip(\n\n /* Fetch version */\n requestJSON(`${url}/releases/latest`)\n .pipe(\n map(release => ({\n version: release.tag_name\n })),\n defaultIfEmpty({})\n ),\n\n /* Fetch stars and forks */\n requestJSON(url)\n .pipe(\n map(info => ({\n stars: info.stargazers_count,\n forks: info.forks_count\n })),\n defaultIfEmpty({})\n )\n )\n .pipe(\n map(([release, info]) => ({ ...release, ...info }))\n )\n\n /* User or organization */\n } else {\n const url = `https://api.github.com/users/${user}`\n return requestJSON(url)\n .pipe(\n map(info => ({\n repositories: info.public_repos\n })),\n defaultIfEmpty({})\n )\n }\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { ProjectSchema } from \"gitlab\"\nimport {\n Observable,\n defaultIfEmpty,\n map\n} from \"rxjs\"\n\nimport { requestJSON } from \"~/browser\"\n\nimport { SourceFacts } from \"../_\"\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Fetch GitLab repository facts\n *\n * @param base - GitLab base\n * @param project - GitLab project\n *\n * @returns Repository facts observable\n */\nexport function fetchSourceFactsFromGitLab(\n base: string, project: string\n): Observable {\n const url = `https://${base}/api/v4/projects/${encodeURIComponent(project)}`\n return requestJSON(url)\n .pipe(\n map(({ star_count, forks_count }) => ({\n stars: star_count,\n forks: forks_count\n })),\n defaultIfEmpty({})\n )\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { EMPTY, Observable } from \"rxjs\"\n\nimport { fetchSourceFactsFromGitHub } from \"../github\"\nimport { fetchSourceFactsFromGitLab } from \"../gitlab\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Repository facts for repositories\n */\nexport interface RepositoryFacts {\n stars?: number /* Number of stars */\n forks?: number /* Number of forks */\n version?: string /* Latest version */\n}\n\n/**\n * Repository facts for organizations\n */\nexport interface OrganizationFacts {\n repositories?: number /* Number of repositories */\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Repository facts\n */\nexport type SourceFacts =\n | RepositoryFacts\n | OrganizationFacts\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Fetch repository facts\n *\n * @param url - Repository URL\n *\n * @returns Repository facts observable\n */\nexport function fetchSourceFacts(\n url: string\n): Observable {\n const [type] = url.match(/(git(?:hub|lab))/i) || []\n switch (type.toLowerCase()) {\n\n /* GitHub repository */\n case \"github\":\n const [, user, repo] = url.match(/^.+github\\.com\\/([^/]+)\\/?([^/]+)?/i)!\n return fetchSourceFactsFromGitHub(user, repo)\n\n /* GitLab repository */\n case \"gitlab\":\n const [, base, slug] = url.match(/^.+?([^/]*gitlab[^/]+)\\/(.+?)\\/?$/i)!\n return fetchSourceFactsFromGitLab(base, slug)\n\n /* Everything else */\n default:\n return EMPTY\n }\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n EMPTY,\n Observable,\n Subject,\n catchError,\n defer,\n filter,\n finalize,\n map,\n of,\n shareReplay,\n tap\n} from \"rxjs\"\n\nimport { getElement } from \"~/browser\"\nimport { renderSourceFacts } from \"~/templates\"\n\nimport { Component } from \"../../_\"\nimport {\n SourceFacts,\n fetchSourceFacts\n} from \"../facts\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Repository information\n */\nexport interface Source {\n facts: SourceFacts /* Repository facts */\n}\n\n/* ----------------------------------------------------------------------------\n * Data\n * ------------------------------------------------------------------------- */\n\n/**\n * Repository information observable\n */\nlet fetch$: Observable\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch repository information\n *\n * This function tries to read the repository facts from session storage, and\n * if unsuccessful, fetches them from the underlying provider.\n *\n * @param el - Repository information element\n *\n * @returns Repository information observable\n */\nexport function watchSource(\n el: HTMLAnchorElement\n): Observable {\n return fetch$ ||= defer(() => {\n const cached = __md_get(\"__source\", sessionStorage)\n if (cached)\n return of(cached)\n else\n return fetchSourceFacts(el.href)\n .pipe(\n tap(facts => __md_set(\"__source\", facts, sessionStorage))\n )\n })\n .pipe(\n catchError(() => EMPTY),\n filter(facts => Object.keys(facts).length > 0),\n map(facts => ({ facts })),\n shareReplay(1)\n )\n}\n\n/**\n * Mount repository information\n *\n * @param el - Repository information element\n *\n * @returns Repository information component observable\n */\nexport function mountSource(\n el: HTMLAnchorElement\n): Observable> {\n const inner = getElement(\":scope > :last-child\", el)\n return defer(() => {\n const push$ = new Subject()\n push$.subscribe(({ facts }) => {\n inner.appendChild(renderSourceFacts(facts))\n inner.setAttribute(\"data-md-state\", \"done\")\n })\n\n /* Create and return component */\n return watchSource(el)\n .pipe(\n tap(state => push$.next(state)),\n finalize(() => push$.complete()),\n map(state => ({ ref: el, ...state }))\n )\n })\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n Observable,\n Subject,\n defer,\n distinctUntilKeyChanged,\n finalize,\n map,\n of,\n switchMap,\n tap\n} from \"rxjs\"\n\nimport { feature } from \"~/_\"\nimport {\n Viewport,\n watchElementSize,\n watchViewportAt\n} from \"~/browser\"\n\nimport { Component } from \"../_\"\nimport { Header } from \"../header\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Navigation tabs\n */\nexport interface Tabs {\n hidden: boolean /* Navigation tabs are hidden */\n}\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch options\n */\ninterface WatchOptions {\n viewport$: Observable /* Viewport observable */\n header$: Observable
    /* Header observable */\n}\n\n/**\n * Mount options\n */\ninterface MountOptions {\n viewport$: Observable /* Viewport observable */\n header$: Observable
    /* Header observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch navigation tabs\n *\n * @param el - Navigation tabs element\n * @param options - Options\n *\n * @returns Navigation tabs observable\n */\nexport function watchTabs(\n el: HTMLElement, { viewport$, header$ }: WatchOptions\n): Observable {\n return watchElementSize(document.body)\n .pipe(\n switchMap(() => watchViewportAt(el, { header$, viewport$ })),\n map(({ offset: { y } }) => {\n return {\n hidden: y >= 10\n }\n }),\n distinctUntilKeyChanged(\"hidden\")\n )\n}\n\n/**\n * Mount navigation tabs\n *\n * This function hides the navigation tabs when scrolling past the threshold\n * and makes them reappear in a nice CSS animation when scrolling back up.\n *\n * @param el - Navigation tabs element\n * @param options - Options\n *\n * @returns Navigation tabs component observable\n */\nexport function mountTabs(\n el: HTMLElement, options: MountOptions\n): Observable> {\n return defer(() => {\n const push$ = new Subject()\n push$.subscribe({\n\n /* Handle emission */\n next({ hidden }) {\n if (hidden)\n el.setAttribute(\"data-md-state\", \"hidden\")\n else\n el.removeAttribute(\"data-md-state\")\n },\n\n /* Handle complete */\n complete() {\n el.removeAttribute(\"data-md-state\")\n }\n })\n\n /* Create and return component */\n return (\n feature(\"navigation.tabs.sticky\")\n ? of({ hidden: false })\n : watchTabs(el, options)\n )\n .pipe(\n tap(state => push$.next(state)),\n finalize(() => push$.complete()),\n map(state => ({ ref: el, ...state }))\n )\n })\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n Observable,\n Subject,\n bufferCount,\n combineLatestWith,\n debounceTime,\n defer,\n distinctUntilChanged,\n distinctUntilKeyChanged,\n finalize,\n map,\n of,\n repeat,\n scan,\n share,\n skip,\n startWith,\n switchMap,\n takeLast,\n takeUntil,\n tap,\n withLatestFrom\n} from \"rxjs\"\n\nimport { feature } from \"~/_\"\nimport {\n Viewport,\n getElement,\n getElements,\n getLocation,\n getOptionalElement,\n watchElementSize\n} from \"~/browser\"\n\nimport {\n Component,\n getComponentElement\n} from \"../_\"\nimport { Header } from \"../header\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Table of contents\n */\nexport interface TableOfContents {\n prev: HTMLAnchorElement[][] /* Anchors (previous) */\n next: HTMLAnchorElement[][] /* Anchors (next) */\n}\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch options\n */\ninterface WatchOptions {\n viewport$: Observable /* Viewport observable */\n header$: Observable
    /* Header observable */\n}\n\n/**\n * Mount options\n */\ninterface MountOptions {\n viewport$: Observable /* Viewport observable */\n header$: Observable
    /* Header observable */\n target$: Observable /* Location target observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch table of contents\n *\n * This is effectively a scroll spy implementation which will account for the\n * fixed header and automatically re-calculate anchor offsets when the viewport\n * is resized. The returned observable will only emit if the table of contents\n * needs to be repainted.\n *\n * This implementation tracks an anchor element's entire path starting from its\n * level up to the top-most anchor element, e.g. `[h3, h2, h1]`. Although the\n * Material theme currently doesn't make use of this information, it enables\n * the styling of the entire hierarchy through customization.\n *\n * Note that the current anchor is the last item of the `prev` anchor list.\n *\n * @param el - Table of contents element\n * @param options - Options\n *\n * @returns Table of contents observable\n */\nexport function watchTableOfContents(\n el: HTMLElement, { viewport$, header$ }: WatchOptions\n): Observable {\n const table = new Map()\n\n /* Compute anchor-to-target mapping */\n const anchors = getElements(\"[href^=\\\\#]\", el)\n for (const anchor of anchors) {\n const id = decodeURIComponent(anchor.hash.substring(1))\n const target = getOptionalElement(`[id=\"${id}\"]`)\n if (typeof target !== \"undefined\")\n table.set(anchor, target)\n }\n\n /* Compute necessary adjustment for header */\n const adjust$ = header$\n .pipe(\n distinctUntilKeyChanged(\"height\"),\n map(({ height }) => {\n const main = getComponentElement(\"main\")\n const grid = getElement(\":scope > :first-child\", main)\n return height + 0.8 * (\n grid.offsetTop -\n main.offsetTop\n )\n }),\n share()\n )\n\n /* Compute partition of previous and next anchors */\n const partition$ = watchElementSize(document.body)\n .pipe(\n distinctUntilKeyChanged(\"height\"),\n\n /* Build index to map anchor paths to vertical offsets */\n switchMap(body => defer(() => {\n let path: HTMLAnchorElement[] = []\n return of([...table].reduce((index, [anchor, target]) => {\n while (path.length) {\n const last = table.get(path[path.length - 1])!\n if (last.tagName >= target.tagName) {\n path.pop()\n } else {\n break\n }\n }\n\n /* If the current anchor is hidden, continue with its parent */\n let offset = target.offsetTop\n while (!offset && target.parentElement) {\n target = target.parentElement\n offset = target.offsetTop\n }\n\n /* Map reversed anchor path to vertical offset */\n return index.set(\n [...path = [...path, anchor]].reverse(),\n offset\n )\n }, new Map()))\n })\n .pipe(\n\n /* Sort index by vertical offset (see https://bit.ly/30z6QSO) */\n map(index => new Map([...index].sort(([, a], [, b]) => a - b))),\n combineLatestWith(adjust$),\n\n /* Re-compute partition when viewport offset changes */\n switchMap(([index, adjust]) => viewport$\n .pipe(\n scan(([prev, next], { offset: { y }, size }) => {\n const last = y + size.height >= Math.floor(body.height)\n\n /* Look forward */\n while (next.length) {\n const [, offset] = next[0]\n if (offset - adjust < y || last) {\n prev = [...prev, next.shift()!]\n } else {\n break\n }\n }\n\n /* Look backward */\n while (prev.length) {\n const [, offset] = prev[prev.length - 1]\n if (offset - adjust >= y && !last) {\n next = [prev.pop()!, ...next]\n } else {\n break\n }\n }\n\n /* Return partition */\n return [prev, next]\n }, [[], [...index]]),\n distinctUntilChanged((a, b) => (\n a[0] === b[0] &&\n a[1] === b[1]\n ))\n )\n )\n )\n )\n )\n\n /* Compute and return anchor list migrations */\n return partition$\n .pipe(\n map(([prev, next]) => ({\n prev: prev.map(([path]) => path),\n next: next.map(([path]) => path)\n })),\n\n /* Extract anchor list migrations */\n startWith({ prev: [], next: [] }),\n bufferCount(2, 1),\n map(([a, b]) => {\n\n /* Moving down */\n if (a.prev.length < b.prev.length) {\n return {\n prev: b.prev.slice(Math.max(0, a.prev.length - 1), b.prev.length),\n next: []\n }\n\n /* Moving up */\n } else {\n return {\n prev: b.prev.slice(-1),\n next: b.next.slice(0, b.next.length - a.next.length)\n }\n }\n })\n )\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Mount table of contents\n *\n * @param el - Table of contents element\n * @param options - Options\n *\n * @returns Table of contents component observable\n */\nexport function mountTableOfContents(\n el: HTMLElement, { viewport$, header$, target$ }: MountOptions\n): Observable> {\n return defer(() => {\n const push$ = new Subject()\n push$.subscribe(({ prev, next }) => {\n\n /* Look forward */\n for (const [anchor] of next) {\n anchor.removeAttribute(\"data-md-state\")\n anchor.classList.remove(\n \"md-nav__link--active\"\n )\n }\n\n /* Look backward */\n for (const [index, [anchor]] of prev.entries()) {\n anchor.setAttribute(\"data-md-state\", \"blur\")\n anchor.classList.toggle(\n \"md-nav__link--active\",\n index === prev.length - 1\n )\n }\n })\n\n /* Set up anchor tracking, if enabled */\n if (feature(\"navigation.tracking\"))\n viewport$\n .pipe(\n takeUntil(push$.pipe(takeLast(1))),\n distinctUntilKeyChanged(\"offset\"),\n debounceTime(250),\n skip(1),\n takeUntil(target$.pipe(skip(1))),\n repeat({ delay: 250 }),\n withLatestFrom(push$)\n )\n .subscribe(([, { prev }]) => {\n const url = getLocation()\n\n /* Set hash fragment to active anchor */\n const anchor = prev[prev.length - 1]\n if (anchor && anchor.length) {\n const [active] = anchor\n const { hash } = new URL(active.href)\n if (url.hash !== hash) {\n url.hash = hash\n history.replaceState({}, \"\", `${url}`)\n }\n\n /* Reset anchor when at the top */\n } else {\n url.hash = \"\"\n history.replaceState({}, \"\", `${url}`)\n }\n })\n\n /* Create and return component */\n return watchTableOfContents(el, { viewport$, header$ })\n .pipe(\n tap(state => push$.next(state)),\n finalize(() => push$.complete()),\n map(state => ({ ref: el, ...state }))\n )\n })\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n Observable,\n Subject,\n bufferCount,\n combineLatest,\n distinctUntilChanged,\n distinctUntilKeyChanged,\n endWith,\n finalize,\n map,\n repeat,\n skip,\n takeLast,\n takeUntil,\n tap\n} from \"rxjs\"\n\nimport { Viewport } from \"~/browser\"\n\nimport { Component } from \"../_\"\nimport { Header } from \"../header\"\nimport { Main } from \"../main\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Back-to-top button\n */\nexport interface BackToTop {\n hidden: boolean /* Back-to-top button is hidden */\n}\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch options\n */\ninterface WatchOptions {\n viewport$: Observable /* Viewport observable */\n main$: Observable
    /* Main area observable */\n target$: Observable /* Location target observable */\n}\n\n/**\n * Mount options\n */\ninterface MountOptions {\n viewport$: Observable /* Viewport observable */\n header$: Observable
    /* Header observable */\n main$: Observable
    /* Main area observable */\n target$: Observable /* Location target observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch back-to-top\n *\n * @param _el - Back-to-top element\n * @param options - Options\n *\n * @returns Back-to-top observable\n */\nexport function watchBackToTop(\n _el: HTMLElement, { viewport$, main$, target$ }: WatchOptions\n): Observable {\n\n /* Compute direction */\n const direction$ = viewport$\n .pipe(\n map(({ offset: { y } }) => y),\n bufferCount(2, 1),\n map(([a, b]) => a > b && b > 0),\n distinctUntilChanged()\n )\n\n /* Compute whether main area is active */\n const active$ = main$\n .pipe(\n map(({ active }) => active)\n )\n\n /* Compute threshold for hiding */\n return combineLatest([active$, direction$])\n .pipe(\n map(([active, direction]) => !(active && direction)),\n distinctUntilChanged(),\n takeUntil(target$.pipe(skip(1))),\n endWith(true),\n repeat({ delay: 250 }),\n map(hidden => ({ hidden }))\n )\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Mount back-to-top\n *\n * @param el - Back-to-top element\n * @param options - Options\n *\n * @returns Back-to-top component observable\n */\nexport function mountBackToTop(\n el: HTMLElement, { viewport$, header$, main$, target$ }: MountOptions\n): Observable> {\n const push$ = new Subject()\n push$.subscribe({\n\n /* Handle emission */\n next({ hidden }) {\n if (hidden) {\n el.setAttribute(\"data-md-state\", \"hidden\")\n el.setAttribute(\"tabindex\", \"-1\")\n el.blur()\n } else {\n el.removeAttribute(\"data-md-state\")\n el.removeAttribute(\"tabindex\")\n }\n },\n\n /* Handle complete */\n complete() {\n el.style.top = \"\"\n el.setAttribute(\"data-md-state\", \"hidden\")\n el.removeAttribute(\"tabindex\")\n }\n })\n\n /* Watch header height */\n header$\n .pipe(\n takeUntil(push$.pipe(endWith(0), takeLast(1))),\n distinctUntilKeyChanged(\"height\")\n )\n .subscribe(({ height }) => {\n el.style.top = `${height + 16}px`\n })\n\n /* Create and return component */\n return watchBackToTop(el, { viewport$, main$, target$ })\n .pipe(\n tap(state => push$.next(state)),\n finalize(() => push$.complete()),\n map(state => ({ ref: el, ...state }))\n )\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n Observable,\n fromEvent,\n mapTo,\n mergeMap,\n switchMap,\n takeWhile,\n tap,\n withLatestFrom\n} from \"rxjs\"\n\nimport { getElements } from \"~/browser\"\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Patch options\n */\ninterface PatchOptions {\n document$: Observable /* Document observable */\n tablet$: Observable /* Media tablet observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Patch indeterminate checkboxes\n *\n * This function replaces the indeterminate \"pseudo state\" with the actual\n * indeterminate state, which is used to keep navigation always expanded.\n *\n * @param options - Options\n */\nexport function patchIndeterminate(\n { document$, tablet$ }: PatchOptions\n): void {\n document$\n .pipe(\n switchMap(() => getElements(\n \"[data-md-state=indeterminate]\"\n )),\n tap(el => {\n el.indeterminate = true\n el.checked = false\n }),\n mergeMap(el => fromEvent(el, \"change\")\n .pipe(\n takeWhile(() => el.hasAttribute(\"data-md-state\")),\n mapTo(el)\n )\n ),\n withLatestFrom(tablet$)\n )\n .subscribe(([el, tablet]) => {\n el.removeAttribute(\"data-md-state\")\n if (tablet)\n el.checked = false\n })\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n Observable,\n filter,\n fromEvent,\n mapTo,\n mergeMap,\n switchMap,\n tap\n} from \"rxjs\"\n\nimport { getElements } from \"~/browser\"\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Patch options\n */\ninterface PatchOptions {\n document$: Observable /* Document observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Helper functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Check whether the given device is an Apple device\n *\n * @returns Test result\n */\nfunction isAppleDevice(): boolean {\n return /(iPad|iPhone|iPod)/.test(navigator.userAgent)\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Patch all elements with `data-md-scrollfix` attributes\n *\n * This is a year-old patch which ensures that overflow scrolling works at the\n * top and bottom of containers on iOS by ensuring a `1px` scroll offset upon\n * the start of a touch event.\n *\n * @see https://bit.ly/2SCtAOO - Original source\n *\n * @param options - Options\n */\nexport function patchScrollfix(\n { document$ }: PatchOptions\n): void {\n document$\n .pipe(\n switchMap(() => getElements(\"[data-md-scrollfix]\")),\n tap(el => el.removeAttribute(\"data-md-scrollfix\")),\n filter(isAppleDevice),\n mergeMap(el => fromEvent(el, \"touchstart\")\n .pipe(\n mapTo(el)\n )\n )\n )\n .subscribe(el => {\n const top = el.scrollTop\n\n /* We're at the top of the container */\n if (top === 0) {\n el.scrollTop = 1\n\n /* We're at the bottom of the container */\n } else if (top + el.offsetHeight === el.scrollHeight) {\n el.scrollTop = top - 1\n }\n })\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n Observable,\n combineLatest,\n delay,\n map,\n of,\n switchMap,\n withLatestFrom\n} from \"rxjs\"\n\nimport {\n Viewport,\n watchToggle\n} from \"~/browser\"\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Patch options\n */\ninterface PatchOptions {\n viewport$: Observable /* Viewport observable */\n tablet$: Observable /* Media tablet observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Patch the document body to lock when search is open\n *\n * For mobile and tablet viewports, the search is rendered full screen, which\n * leads to scroll leaking when at the top or bottom of the search result. This\n * function locks the body when the search is in full screen mode, and restores\n * the scroll position when leaving.\n *\n * @param options - Options\n */\nexport function patchScrolllock(\n { viewport$, tablet$ }: PatchOptions\n): void {\n combineLatest([watchToggle(\"search\"), tablet$])\n .pipe(\n map(([active, tablet]) => active && !tablet),\n switchMap(active => of(active)\n .pipe(\n delay(active ? 400 : 100)\n )\n ),\n withLatestFrom(viewport$)\n )\n .subscribe(([active, { offset: { y }}]) => {\n if (active) {\n document.body.setAttribute(\"data-md-state\", \"lock\")\n document.body.style.top = `-${y}px`\n } else {\n const value = -1 * parseInt(document.body.style.top, 10)\n document.body.removeAttribute(\"data-md-state\")\n document.body.style.top = \"\"\n if (value)\n window.scrollTo(0, value)\n }\n })\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\n/* ----------------------------------------------------------------------------\n * Polyfills\n * ------------------------------------------------------------------------- */\n\n/* Polyfill `Object.entries` */\nif (!Object.entries)\n Object.entries = function (obj: object) {\n const data: [string, string][] = []\n for (const key of Object.keys(obj))\n // @ts-expect-error - ignore property access warning\n data.push([key, obj[key]])\n\n /* Return entries */\n return data\n }\n\n/* Polyfill `Object.values` */\nif (!Object.values)\n Object.values = function (obj: object) {\n const data: string[] = []\n for (const key of Object.keys(obj))\n // @ts-expect-error - ignore property access warning\n data.push(obj[key])\n\n /* Return values */\n return data\n }\n\n/* ------------------------------------------------------------------------- */\n\n/* Polyfills for `Element` */\nif (typeof Element !== \"undefined\") {\n\n /* Polyfill `Element.scrollTo` */\n if (!Element.prototype.scrollTo)\n Element.prototype.scrollTo = function (\n x?: ScrollToOptions | number, y?: number\n ): void {\n if (typeof x === \"object\") {\n this.scrollLeft = x.left!\n this.scrollTop = x.top!\n } else {\n this.scrollLeft = x!\n this.scrollTop = y!\n }\n }\n\n /* Polyfill `Element.replaceWith` */\n if (!Element.prototype.replaceWith)\n Element.prototype.replaceWith = function (\n ...nodes: Array\n ): void {\n const parent = this.parentNode\n if (parent) {\n if (nodes.length === 0)\n parent.removeChild(this)\n\n /* Replace children and create text nodes */\n for (let i = nodes.length - 1; i >= 0; i--) {\n let node = nodes[i]\n if (typeof node !== \"object\")\n node = document.createTextNode(node)\n else if (node.parentNode)\n node.parentNode.removeChild(node)\n\n /* Replace child or insert before previous sibling */\n if (!i)\n parent.replaceChild(node, this)\n else\n parent.insertBefore(this.previousSibling!, node)\n }\n }\n }\n}\n"], + "mappings": "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", + "names": [] +} diff --git a/v0.13.6/assets/javascripts/lunr/min/lunr.ar.min.js b/v0.13.6/assets/javascripts/lunr/min/lunr.ar.min.js new file mode 100644 index 0000000000..248ddc5d14 --- /dev/null +++ b/v0.13.6/assets/javascripts/lunr/min/lunr.ar.min.js @@ -0,0 +1 @@ +!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.ar=function(){this.pipeline.reset(),this.pipeline.add(e.ar.trimmer,e.ar.stopWordFilter,e.ar.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.ar.stemmer))},e.ar.wordCharacters="ء-ٛٱـ",e.ar.trimmer=e.trimmerSupport.generateTrimmer(e.ar.wordCharacters),e.Pipeline.registerFunction(e.ar.trimmer,"trimmer-ar"),e.ar.stemmer=function(){var e=this;return e.result=!1,e.preRemoved=!1,e.sufRemoved=!1,e.pre={pre1:"ف ك ب و س ل ن ا ي ت",pre2:"ال لل",pre3:"بال وال فال تال كال ولل",pre4:"فبال كبال وبال وكال"},e.suf={suf1:"ه ك ت ن ا ي",suf2:"نك نه ها وك يا اه ون ين تن تم نا وا ان كم كن ني نن ما هم هن تك ته ات يه",suf3:"تين كهم نيه نهم ونه وها يهم ونا ونك وني وهم تكم تنا تها تني تهم كما كها ناه نكم هنا تان يها",suf4:"كموه ناها ونني ونهم تكما تموه تكاه كماه ناكم ناهم نيها وننا"},e.patterns=JSON.parse('{"pt43":[{"pt":[{"c":"ا","l":1}]},{"pt":[{"c":"ا,ت,ن,ي","l":0}],"mPt":[{"c":"ف","l":0,"m":1},{"c":"ع","l":1,"m":2},{"c":"ل","l":2,"m":3}]},{"pt":[{"c":"و","l":2}],"mPt":[{"c":"ف","l":0,"m":0},{"c":"ع","l":1,"m":1},{"c":"ل","l":2,"m":3}]},{"pt":[{"c":"ا","l":2}]},{"pt":[{"c":"ي","l":2}],"mPt":[{"c":"ف","l":0,"m":0},{"c":"ع","l":1,"m":1},{"c":"ا","l":2},{"c":"ل","l":3,"m":3}]},{"pt":[{"c":"م","l":0}]}],"pt53":[{"pt":[{"c":"ت","l":0},{"c":"ا","l":2}]},{"pt":[{"c":"ا,ن,ت,ي","l":0},{"c":"ت","l":2}],"mPt":[{"c":"ا","l":0},{"c":"ف","l":1,"m":1},{"c":"ت","l":2},{"c":"ع","l":3,"m":3},{"c":"ا","l":4},{"c":"ل","l":5,"m":4}]},{"pt":[{"c":"ا","l":0},{"c":"ا","l":2}],"mPt":[{"c":"ا","l":0},{"c":"ف","l":1,"m":1},{"c":"ع","l":2,"m":3},{"c":"ل","l":3,"m":4},{"c":"ا","l":4},{"c":"ل","l":5,"m":4}]},{"pt":[{"c":"ا","l":0},{"c":"ا","l":3}],"mPt":[{"c":"ف","l":0,"m":1},{"c":"ع","l":1,"m":2},{"c":"ل","l":2,"m":4}]},{"pt":[{"c":"ا","l":3},{"c":"ن","l":4}]},{"pt":[{"c":"ت","l":0},{"c":"ي","l":3}]},{"pt":[{"c":"م","l":0},{"c":"و","l":3}]},{"pt":[{"c":"ا","l":1},{"c":"و","l":3}]},{"pt":[{"c":"و","l":1},{"c":"ا","l":2}]},{"pt":[{"c":"م","l":0},{"c":"ا","l":3}]},{"pt":[{"c":"م","l":0},{"c":"ي","l":3}]},{"pt":[{"c":"ا","l":2},{"c":"ن","l":3}]},{"pt":[{"c":"م","l":0},{"c":"ن","l":1}],"mPt":[{"c":"ا","l":0},{"c":"ن","l":1},{"c":"ف","l":2,"m":2},{"c":"ع","l":3,"m":3},{"c":"ا","l":4},{"c":"ل","l":5,"m":4}]},{"pt":[{"c":"م","l":0},{"c":"ت","l":2}],"mPt":[{"c":"ا","l":0},{"c":"ف","l":1,"m":1},{"c":"ت","l":2},{"c":"ع","l":3,"m":3},{"c":"ا","l":4},{"c":"ل","l":5,"m":4}]},{"pt":[{"c":"م","l":0},{"c":"ا","l":2}]},{"pt":[{"c":"م","l":1},{"c":"ا","l":3}]},{"pt":[{"c":"ي,ت,ا,ن","l":0},{"c":"ت","l":1}],"mPt":[{"c":"ف","l":0,"m":2},{"c":"ع","l":1,"m":3},{"c":"ا","l":2},{"c":"ل","l":3,"m":4}]},{"pt":[{"c":"ت,ي,ا,ن","l":0},{"c":"ت","l":2}],"mPt":[{"c":"ا","l":0},{"c":"ف","l":1,"m":1},{"c":"ت","l":2},{"c":"ع","l":3,"m":3},{"c":"ا","l":4},{"c":"ل","l":5,"m":4}]},{"pt":[{"c":"ا","l":2},{"c":"ي","l":3}]},{"pt":[{"c":"ا,ي,ت,ن","l":0},{"c":"ن","l":1}],"mPt":[{"c":"ا","l":0},{"c":"ن","l":1},{"c":"ف","l":2,"m":2},{"c":"ع","l":3,"m":3},{"c":"ا","l":4},{"c":"ل","l":5,"m":4}]},{"pt":[{"c":"ا","l":3},{"c":"ء","l":4}]}],"pt63":[{"pt":[{"c":"ا","l":0},{"c":"ت","l":2},{"c":"ا","l":4}]},{"pt":[{"c":"ا,ت,ن,ي","l":0},{"c":"س","l":1},{"c":"ت","l":2}],"mPt":[{"c":"ا","l":0},{"c":"س","l":1},{"c":"ت","l":2},{"c":"ف","l":3,"m":3},{"c":"ع","l":4,"m":4},{"c":"ا","l":5},{"c":"ل","l":6,"m":5}]},{"pt":[{"c":"ا,ن,ت,ي","l":0},{"c":"و","l":3}]},{"pt":[{"c":"م","l":0},{"c":"س","l":1},{"c":"ت","l":2}],"mPt":[{"c":"ا","l":0},{"c":"س","l":1},{"c":"ت","l":2},{"c":"ف","l":3,"m":3},{"c":"ع","l":4,"m":4},{"c":"ا","l":5},{"c":"ل","l":6,"m":5}]},{"pt":[{"c":"ي","l":1},{"c":"ي","l":3},{"c":"ا","l":4},{"c":"ء","l":5}]},{"pt":[{"c":"ا","l":0},{"c":"ن","l":1},{"c":"ا","l":4}]}],"pt54":[{"pt":[{"c":"ت","l":0}]},{"pt":[{"c":"ا,ي,ت,ن","l":0}],"mPt":[{"c":"ا","l":0},{"c":"ف","l":1,"m":1},{"c":"ع","l":2,"m":2},{"c":"ل","l":3,"m":3},{"c":"ر","l":4,"m":4},{"c":"ا","l":5},{"c":"ر","l":6,"m":4}]},{"pt":[{"c":"م","l":0}],"mPt":[{"c":"ا","l":0},{"c":"ف","l":1,"m":1},{"c":"ع","l":2,"m":2},{"c":"ل","l":3,"m":3},{"c":"ر","l":4,"m":4},{"c":"ا","l":5},{"c":"ر","l":6,"m":4}]},{"pt":[{"c":"ا","l":2}]},{"pt":[{"c":"ا","l":0},{"c":"ن","l":2}]}],"pt64":[{"pt":[{"c":"ا","l":0},{"c":"ا","l":4}]},{"pt":[{"c":"م","l":0},{"c":"ت","l":1}]}],"pt73":[{"pt":[{"c":"ا","l":0},{"c":"س","l":1},{"c":"ت","l":2},{"c":"ا","l":5}]}],"pt75":[{"pt":[{"c":"ا","l":0},{"c":"ا","l":5}]}]}'),e.execArray=["cleanWord","removeDiacritics","cleanAlef","removeStopWords","normalizeHamzaAndAlef","removeStartWaw","removePre432","removeEndTaa","wordCheck"],e.stem=function(){var r=0;for(e.result=!1,e.preRemoved=!1,e.sufRemoved=!1;r=0)return!0},e.normalizeHamzaAndAlef=function(){return e.word=e.word.replace("ؤ","ء"),e.word=e.word.replace("ئ","ء"),e.word=e.word.replace(/([\u0627])\1+/gi,"ا"),!1},e.removeEndTaa=function(){return!(e.word.length>2)||(e.word=e.word.replace(/[\u0627]$/,""),e.word=e.word.replace("ة",""),!1)},e.removeStartWaw=function(){return e.word.length>3&&"و"==e.word[0]&&"و"==e.word[1]&&(e.word=e.word.slice(1)),!1},e.removePre432=function(){var r=e.word;if(e.word.length>=7){var t=new RegExp("^("+e.pre.pre4.split(" ").join("|")+")");e.word=e.word.replace(t,"")}if(e.word==r&&e.word.length>=6){var c=new RegExp("^("+e.pre.pre3.split(" ").join("|")+")");e.word=e.word.replace(c,"")}if(e.word==r&&e.word.length>=5){var l=new RegExp("^("+e.pre.pre2.split(" ").join("|")+")");e.word=e.word.replace(l,"")}return r!=e.word&&(e.preRemoved=!0),!1},e.patternCheck=function(r){for(var t=0;t3){var t=new RegExp("^("+e.pre.pre1.split(" ").join("|")+")");e.word=e.word.replace(t,"")}return r!=e.word&&(e.preRemoved=!0),!1},e.removeSuf1=function(){var r=e.word;if(0==e.sufRemoved&&e.word.length>3){var t=new RegExp("("+e.suf.suf1.split(" ").join("|")+")$");e.word=e.word.replace(t,"")}return r!=e.word&&(e.sufRemoved=!0),!1},e.removeSuf432=function(){var r=e.word;if(e.word.length>=6){var t=new RegExp("("+e.suf.suf4.split(" ").join("|")+")$");e.word=e.word.replace(t,"")}if(e.word==r&&e.word.length>=5){var c=new RegExp("("+e.suf.suf3.split(" ").join("|")+")$");e.word=e.word.replace(c,"")}if(e.word==r&&e.word.length>=4){var l=new RegExp("("+e.suf.suf2.split(" ").join("|")+")$");e.word=e.word.replace(l,"")}return r!=e.word&&(e.sufRemoved=!0),!1},e.wordCheck=function(){for(var r=(e.word,[e.removeSuf432,e.removeSuf1,e.removePre1]),t=0,c=!1;e.word.length>=7&&!e.result&&t=f.limit)return;f.cursor++}for(;!f.out_grouping(w,97,248);){if(f.cursor>=f.limit)return;f.cursor++}d=f.cursor,d=d&&(r=f.limit_backward,f.limit_backward=d,f.ket=f.cursor,e=f.find_among_b(c,32),f.limit_backward=r,e))switch(f.bra=f.cursor,e){case 1:f.slice_del();break;case 2:f.in_grouping_b(p,97,229)&&f.slice_del()}}function t(){var e,r=f.limit-f.cursor;f.cursor>=d&&(e=f.limit_backward,f.limit_backward=d,f.ket=f.cursor,f.find_among_b(l,4)?(f.bra=f.cursor,f.limit_backward=e,f.cursor=f.limit-r,f.cursor>f.limit_backward&&(f.cursor--,f.bra=f.cursor,f.slice_del())):f.limit_backward=e)}function s(){var e,r,i,n=f.limit-f.cursor;if(f.ket=f.cursor,f.eq_s_b(2,"st")&&(f.bra=f.cursor,f.eq_s_b(2,"ig")&&f.slice_del()),f.cursor=f.limit-n,f.cursor>=d&&(r=f.limit_backward,f.limit_backward=d,f.ket=f.cursor,e=f.find_among_b(m,5),f.limit_backward=r,e))switch(f.bra=f.cursor,e){case 1:f.slice_del(),i=f.limit-f.cursor,t(),f.cursor=f.limit-i;break;case 2:f.slice_from("løs")}}function o(){var e;f.cursor>=d&&(e=f.limit_backward,f.limit_backward=d,f.ket=f.cursor,f.out_grouping_b(w,97,248)?(f.bra=f.cursor,u=f.slice_to(u),f.limit_backward=e,f.eq_v_b(u)&&f.slice_del()):f.limit_backward=e)}var a,d,u,c=[new r("hed",-1,1),new r("ethed",0,1),new r("ered",-1,1),new r("e",-1,1),new r("erede",3,1),new r("ende",3,1),new r("erende",5,1),new r("ene",3,1),new r("erne",3,1),new r("ere",3,1),new r("en",-1,1),new r("heden",10,1),new r("eren",10,1),new r("er",-1,1),new r("heder",13,1),new r("erer",13,1),new r("s",-1,2),new r("heds",16,1),new r("es",16,1),new r("endes",18,1),new r("erendes",19,1),new r("enes",18,1),new r("ernes",18,1),new r("eres",18,1),new r("ens",16,1),new r("hedens",24,1),new r("erens",24,1),new r("ers",16,1),new r("ets",16,1),new r("erets",28,1),new r("et",-1,1),new r("eret",30,1)],l=[new r("gd",-1,-1),new r("dt",-1,-1),new r("gt",-1,-1),new r("kt",-1,-1)],m=[new r("ig",-1,1),new r("lig",0,1),new r("elig",1,1),new r("els",-1,1),new r("løst",-1,2)],w=[17,65,16,1,0,0,0,0,0,0,0,0,0,0,0,0,48,0,128],p=[239,254,42,3,0,0,0,0,0,0,0,0,0,0,0,0,16],f=new i;this.setCurrent=function(e){f.setCurrent(e)},this.getCurrent=function(){return f.getCurrent()},this.stem=function(){var r=f.cursor;return e(),f.limit_backward=r,f.cursor=f.limit,n(),f.cursor=f.limit,t(),f.cursor=f.limit,s(),f.cursor=f.limit,o(),!0}};return function(e){return"function"==typeof e.update?e.update(function(e){return n.setCurrent(e),n.stem(),n.getCurrent()}):(n.setCurrent(e),n.stem(),n.getCurrent())}}(),e.Pipeline.registerFunction(e.da.stemmer,"stemmer-da"),e.da.stopWordFilter=e.generateStopWordFilter("ad af alle alt anden at blev blive bliver da de dem den denne der deres det dette dig din disse dog du efter eller en end er et for fra ham han hans har havde have hende hendes her hos hun hvad hvis hvor i ikke ind jeg jer jo kunne man mange med meget men mig min mine mit mod ned noget nogle nu når og også om op os over på selv sig sin sine sit skal skulle som sådan thi til ud under var vi vil ville vor være været".split(" ")),e.Pipeline.registerFunction(e.da.stopWordFilter,"stopWordFilter-da")}}); \ No newline at end of file diff --git a/v0.13.6/assets/javascripts/lunr/min/lunr.de.min.js b/v0.13.6/assets/javascripts/lunr/min/lunr.de.min.js new file mode 100644 index 0000000000..f3b5c108c9 --- /dev/null +++ b/v0.13.6/assets/javascripts/lunr/min/lunr.de.min.js @@ -0,0 +1,18 @@ +/*! + * Lunr languages, `German` language + * https://github.com/MihaiValentin/lunr-languages + * + * Copyright 2014, Mihai Valentin + * http://www.mozilla.org/MPL/ + */ +/*! + * based on + * Snowball JavaScript Library v0.3 + * http://code.google.com/p/urim/ + * http://snowball.tartarus.org/ + * + * Copyright 2010, Oleg Mazko + * http://www.mozilla.org/MPL/ + */ + +!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.de=function(){this.pipeline.reset(),this.pipeline.add(e.de.trimmer,e.de.stopWordFilter,e.de.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.de.stemmer))},e.de.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",e.de.trimmer=e.trimmerSupport.generateTrimmer(e.de.wordCharacters),e.Pipeline.registerFunction(e.de.trimmer,"trimmer-de"),e.de.stemmer=function(){var r=e.stemmerSupport.Among,n=e.stemmerSupport.SnowballProgram,i=new function(){function e(e,r,n){return!(!v.eq_s(1,e)||(v.ket=v.cursor,!v.in_grouping(p,97,252)))&&(v.slice_from(r),v.cursor=n,!0)}function i(){for(var r,n,i,s,t=v.cursor;;)if(r=v.cursor,v.bra=r,v.eq_s(1,"ß"))v.ket=v.cursor,v.slice_from("ss");else{if(r>=v.limit)break;v.cursor=r+1}for(v.cursor=t;;)for(n=v.cursor;;){if(i=v.cursor,v.in_grouping(p,97,252)){if(s=v.cursor,v.bra=s,e("u","U",i))break;if(v.cursor=s,e("y","Y",i))break}if(i>=v.limit)return void(v.cursor=n);v.cursor=i+1}}function s(){for(;!v.in_grouping(p,97,252);){if(v.cursor>=v.limit)return!0;v.cursor++}for(;!v.out_grouping(p,97,252);){if(v.cursor>=v.limit)return!0;v.cursor++}return!1}function t(){m=v.limit,l=m;var e=v.cursor+3;0<=e&&e<=v.limit&&(d=e,s()||(m=v.cursor,m=v.limit)return;v.cursor++}}}function c(){return m<=v.cursor}function u(){return l<=v.cursor}function a(){var e,r,n,i,s=v.limit-v.cursor;if(v.ket=v.cursor,(e=v.find_among_b(w,7))&&(v.bra=v.cursor,c()))switch(e){case 1:v.slice_del();break;case 2:v.slice_del(),v.ket=v.cursor,v.eq_s_b(1,"s")&&(v.bra=v.cursor,v.eq_s_b(3,"nis")&&v.slice_del());break;case 3:v.in_grouping_b(g,98,116)&&v.slice_del()}if(v.cursor=v.limit-s,v.ket=v.cursor,(e=v.find_among_b(f,4))&&(v.bra=v.cursor,c()))switch(e){case 1:v.slice_del();break;case 2:if(v.in_grouping_b(k,98,116)){var t=v.cursor-3;v.limit_backward<=t&&t<=v.limit&&(v.cursor=t,v.slice_del())}}if(v.cursor=v.limit-s,v.ket=v.cursor,(e=v.find_among_b(_,8))&&(v.bra=v.cursor,u()))switch(e){case 1:v.slice_del(),v.ket=v.cursor,v.eq_s_b(2,"ig")&&(v.bra=v.cursor,r=v.limit-v.cursor,v.eq_s_b(1,"e")||(v.cursor=v.limit-r,u()&&v.slice_del()));break;case 2:n=v.limit-v.cursor,v.eq_s_b(1,"e")||(v.cursor=v.limit-n,v.slice_del());break;case 3:if(v.slice_del(),v.ket=v.cursor,i=v.limit-v.cursor,!v.eq_s_b(2,"er")&&(v.cursor=v.limit-i,!v.eq_s_b(2,"en")))break;v.bra=v.cursor,c()&&v.slice_del();break;case 4:v.slice_del(),v.ket=v.cursor,e=v.find_among_b(b,2),e&&(v.bra=v.cursor,u()&&1==e&&v.slice_del())}}var d,l,m,h=[new r("",-1,6),new r("U",0,2),new r("Y",0,1),new r("ä",0,3),new r("ö",0,4),new r("ü",0,5)],w=[new r("e",-1,2),new r("em",-1,1),new r("en",-1,2),new r("ern",-1,1),new r("er",-1,1),new r("s",-1,3),new r("es",5,2)],f=[new r("en",-1,1),new r("er",-1,1),new r("st",-1,2),new r("est",2,1)],b=[new r("ig",-1,1),new r("lich",-1,1)],_=[new r("end",-1,1),new r("ig",-1,2),new r("ung",-1,1),new r("lich",-1,3),new r("isch",-1,2),new r("ik",-1,2),new r("heit",-1,3),new r("keit",-1,4)],p=[17,65,16,1,0,0,0,0,0,0,0,0,0,0,0,0,8,0,32,8],g=[117,30,5],k=[117,30,4],v=new n;this.setCurrent=function(e){v.setCurrent(e)},this.getCurrent=function(){return v.getCurrent()},this.stem=function(){var e=v.cursor;return i(),v.cursor=e,t(),v.limit_backward=e,v.cursor=v.limit,a(),v.cursor=v.limit_backward,o(),!0}};return function(e){return"function"==typeof e.update?e.update(function(e){return i.setCurrent(e),i.stem(),i.getCurrent()}):(i.setCurrent(e),i.stem(),i.getCurrent())}}(),e.Pipeline.registerFunction(e.de.stemmer,"stemmer-de"),e.de.stopWordFilter=e.generateStopWordFilter("aber alle allem allen aller alles als also am an ander andere anderem anderen anderer anderes anderm andern anderr anders auch auf aus bei bin bis bist da damit dann das dasselbe dazu daß dein deine deinem deinen deiner deines dem demselben den denn denselben der derer derselbe derselben des desselben dessen dich die dies diese dieselbe dieselben diesem diesen dieser dieses dir doch dort du durch ein eine einem einen einer eines einig einige einigem einigen einiger einiges einmal er es etwas euch euer eure eurem euren eurer eures für gegen gewesen hab habe haben hat hatte hatten hier hin hinter ich ihm ihn ihnen ihr ihre ihrem ihren ihrer ihres im in indem ins ist jede jedem jeden jeder jedes jene jenem jenen jener jenes jetzt kann kein keine keinem keinen keiner keines können könnte machen man manche manchem manchen mancher manches mein meine meinem meinen meiner meines mich mir mit muss musste nach nicht nichts noch nun nur ob oder ohne sehr sein seine seinem seinen seiner seines selbst sich sie sind so solche solchem solchen solcher solches soll sollte sondern sonst um und uns unse unsem unsen unser unses unter viel vom von vor war waren warst was weg weil weiter welche welchem welchen welcher welches wenn werde werden wie wieder will wir wird wirst wo wollen wollte während würde würden zu zum zur zwar zwischen über".split(" ")),e.Pipeline.registerFunction(e.de.stopWordFilter,"stopWordFilter-de")}}); \ No newline at end of file diff --git a/v0.13.6/assets/javascripts/lunr/min/lunr.du.min.js b/v0.13.6/assets/javascripts/lunr/min/lunr.du.min.js new file mode 100644 index 0000000000..49a0f3f0ac --- /dev/null +++ b/v0.13.6/assets/javascripts/lunr/min/lunr.du.min.js @@ -0,0 +1,18 @@ +/*! + * Lunr languages, `Dutch` language + * https://github.com/MihaiValentin/lunr-languages + * + * Copyright 2014, Mihai Valentin + * http://www.mozilla.org/MPL/ + */ +/*! + * based on + * Snowball JavaScript Library v0.3 + * http://code.google.com/p/urim/ + * http://snowball.tartarus.org/ + * + * Copyright 2010, Oleg Mazko + * http://www.mozilla.org/MPL/ + */ + +!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");console.warn('[Lunr Languages] Please use the "nl" instead of the "du". The "nl" code is the standard code for Dutch language, and "du" will be removed in the next major versions.'),e.du=function(){this.pipeline.reset(),this.pipeline.add(e.du.trimmer,e.du.stopWordFilter,e.du.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.du.stemmer))},e.du.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",e.du.trimmer=e.trimmerSupport.generateTrimmer(e.du.wordCharacters),e.Pipeline.registerFunction(e.du.trimmer,"trimmer-du"),e.du.stemmer=function(){var r=e.stemmerSupport.Among,i=e.stemmerSupport.SnowballProgram,n=new function(){function e(){for(var e,r,i,o=C.cursor;;){if(C.bra=C.cursor,e=C.find_among(b,11))switch(C.ket=C.cursor,e){case 1:C.slice_from("a");continue;case 2:C.slice_from("e");continue;case 3:C.slice_from("i");continue;case 4:C.slice_from("o");continue;case 5:C.slice_from("u");continue;case 6:if(C.cursor>=C.limit)break;C.cursor++;continue}break}for(C.cursor=o,C.bra=o,C.eq_s(1,"y")?(C.ket=C.cursor,C.slice_from("Y")):C.cursor=o;;)if(r=C.cursor,C.in_grouping(q,97,232)){if(i=C.cursor,C.bra=i,C.eq_s(1,"i"))C.ket=C.cursor,C.in_grouping(q,97,232)&&(C.slice_from("I"),C.cursor=r);else if(C.cursor=i,C.eq_s(1,"y"))C.ket=C.cursor,C.slice_from("Y"),C.cursor=r;else if(n(r))break}else if(n(r))break}function n(e){return C.cursor=e,e>=C.limit||(C.cursor++,!1)}function o(){_=C.limit,f=_,t()||(_=C.cursor,_<3&&(_=3),t()||(f=C.cursor))}function t(){for(;!C.in_grouping(q,97,232);){if(C.cursor>=C.limit)return!0;C.cursor++}for(;!C.out_grouping(q,97,232);){if(C.cursor>=C.limit)return!0;C.cursor++}return!1}function s(){for(var e;;)if(C.bra=C.cursor,e=C.find_among(p,3))switch(C.ket=C.cursor,e){case 1:C.slice_from("y");break;case 2:C.slice_from("i");break;case 3:if(C.cursor>=C.limit)return;C.cursor++}}function u(){return _<=C.cursor}function c(){return f<=C.cursor}function a(){var e=C.limit-C.cursor;C.find_among_b(g,3)&&(C.cursor=C.limit-e,C.ket=C.cursor,C.cursor>C.limit_backward&&(C.cursor--,C.bra=C.cursor,C.slice_del()))}function l(){var e;w=!1,C.ket=C.cursor,C.eq_s_b(1,"e")&&(C.bra=C.cursor,u()&&(e=C.limit-C.cursor,C.out_grouping_b(q,97,232)&&(C.cursor=C.limit-e,C.slice_del(),w=!0,a())))}function m(){var e;u()&&(e=C.limit-C.cursor,C.out_grouping_b(q,97,232)&&(C.cursor=C.limit-e,C.eq_s_b(3,"gem")||(C.cursor=C.limit-e,C.slice_del(),a())))}function d(){var e,r,i,n,o,t,s=C.limit-C.cursor;if(C.ket=C.cursor,e=C.find_among_b(h,5))switch(C.bra=C.cursor,e){case 1:u()&&C.slice_from("heid");break;case 2:m();break;case 3:u()&&C.out_grouping_b(z,97,232)&&C.slice_del()}if(C.cursor=C.limit-s,l(),C.cursor=C.limit-s,C.ket=C.cursor,C.eq_s_b(4,"heid")&&(C.bra=C.cursor,c()&&(r=C.limit-C.cursor,C.eq_s_b(1,"c")||(C.cursor=C.limit-r,C.slice_del(),C.ket=C.cursor,C.eq_s_b(2,"en")&&(C.bra=C.cursor,m())))),C.cursor=C.limit-s,C.ket=C.cursor,e=C.find_among_b(k,6))switch(C.bra=C.cursor,e){case 1:if(c()){if(C.slice_del(),i=C.limit-C.cursor,C.ket=C.cursor,C.eq_s_b(2,"ig")&&(C.bra=C.cursor,c()&&(n=C.limit-C.cursor,!C.eq_s_b(1,"e")))){C.cursor=C.limit-n,C.slice_del();break}C.cursor=C.limit-i,a()}break;case 2:c()&&(o=C.limit-C.cursor,C.eq_s_b(1,"e")||(C.cursor=C.limit-o,C.slice_del()));break;case 3:c()&&(C.slice_del(),l());break;case 4:c()&&C.slice_del();break;case 5:c()&&w&&C.slice_del()}C.cursor=C.limit-s,C.out_grouping_b(j,73,232)&&(t=C.limit-C.cursor,C.find_among_b(v,4)&&C.out_grouping_b(q,97,232)&&(C.cursor=C.limit-t,C.ket=C.cursor,C.cursor>C.limit_backward&&(C.cursor--,C.bra=C.cursor,C.slice_del())))}var f,_,w,b=[new r("",-1,6),new r("á",0,1),new r("ä",0,1),new r("é",0,2),new r("ë",0,2),new r("í",0,3),new r("ï",0,3),new r("ó",0,4),new r("ö",0,4),new r("ú",0,5),new r("ü",0,5)],p=[new r("",-1,3),new r("I",0,2),new r("Y",0,1)],g=[new r("dd",-1,-1),new r("kk",-1,-1),new r("tt",-1,-1)],h=[new r("ene",-1,2),new r("se",-1,3),new r("en",-1,2),new r("heden",2,1),new r("s",-1,3)],k=[new r("end",-1,1),new r("ig",-1,2),new r("ing",-1,1),new r("lijk",-1,3),new r("baar",-1,4),new r("bar",-1,5)],v=[new r("aa",-1,-1),new r("ee",-1,-1),new r("oo",-1,-1),new r("uu",-1,-1)],q=[17,65,16,1,0,0,0,0,0,0,0,0,0,0,0,0,128],j=[1,0,0,17,65,16,1,0,0,0,0,0,0,0,0,0,0,0,0,128],z=[17,67,16,1,0,0,0,0,0,0,0,0,0,0,0,0,128],C=new i;this.setCurrent=function(e){C.setCurrent(e)},this.getCurrent=function(){return C.getCurrent()},this.stem=function(){var r=C.cursor;return e(),C.cursor=r,o(),C.limit_backward=r,C.cursor=C.limit,d(),C.cursor=C.limit_backward,s(),!0}};return function(e){return"function"==typeof e.update?e.update(function(e){return n.setCurrent(e),n.stem(),n.getCurrent()}):(n.setCurrent(e),n.stem(),n.getCurrent())}}(),e.Pipeline.registerFunction(e.du.stemmer,"stemmer-du"),e.du.stopWordFilter=e.generateStopWordFilter(" aan al alles als altijd andere ben bij daar dan dat de der deze die dit doch doen door dus een eens en er ge geen geweest haar had heb hebben heeft hem het hier hij hoe hun iemand iets ik in is ja je kan kon kunnen maar me meer men met mij mijn moet na naar niet niets nog nu of om omdat onder ons ook op over reeds te tegen toch toen tot u uit uw van veel voor want waren was wat werd wezen wie wil worden wordt zal ze zelf zich zij zijn zo zonder zou".split(" ")),e.Pipeline.registerFunction(e.du.stopWordFilter,"stopWordFilter-du")}}); \ No newline at end of file diff --git a/v0.13.6/assets/javascripts/lunr/min/lunr.es.min.js b/v0.13.6/assets/javascripts/lunr/min/lunr.es.min.js new file mode 100644 index 0000000000..2989d34265 --- /dev/null +++ b/v0.13.6/assets/javascripts/lunr/min/lunr.es.min.js @@ -0,0 +1,18 @@ +/*! + * Lunr languages, `Spanish` language + * https://github.com/MihaiValentin/lunr-languages + * + * Copyright 2014, Mihai Valentin + * http://www.mozilla.org/MPL/ + */ +/*! + * based on + * Snowball JavaScript Library v0.3 + * http://code.google.com/p/urim/ + * http://snowball.tartarus.org/ + * + * Copyright 2010, Oleg Mazko + * http://www.mozilla.org/MPL/ + */ + +!function(e,s){"function"==typeof define&&define.amd?define(s):"object"==typeof exports?module.exports=s():s()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.es=function(){this.pipeline.reset(),this.pipeline.add(e.es.trimmer,e.es.stopWordFilter,e.es.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.es.stemmer))},e.es.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",e.es.trimmer=e.trimmerSupport.generateTrimmer(e.es.wordCharacters),e.Pipeline.registerFunction(e.es.trimmer,"trimmer-es"),e.es.stemmer=function(){var s=e.stemmerSupport.Among,r=e.stemmerSupport.SnowballProgram,n=new function(){function e(){if(A.out_grouping(x,97,252)){for(;!A.in_grouping(x,97,252);){if(A.cursor>=A.limit)return!0;A.cursor++}return!1}return!0}function n(){if(A.in_grouping(x,97,252)){var s=A.cursor;if(e()){if(A.cursor=s,!A.in_grouping(x,97,252))return!0;for(;!A.out_grouping(x,97,252);){if(A.cursor>=A.limit)return!0;A.cursor++}}return!1}return!0}function i(){var s,r=A.cursor;if(n()){if(A.cursor=r,!A.out_grouping(x,97,252))return;if(s=A.cursor,e()){if(A.cursor=s,!A.in_grouping(x,97,252)||A.cursor>=A.limit)return;A.cursor++}}g=A.cursor}function a(){for(;!A.in_grouping(x,97,252);){if(A.cursor>=A.limit)return!1;A.cursor++}for(;!A.out_grouping(x,97,252);){if(A.cursor>=A.limit)return!1;A.cursor++}return!0}function t(){var e=A.cursor;g=A.limit,p=g,v=g,i(),A.cursor=e,a()&&(p=A.cursor,a()&&(v=A.cursor))}function o(){for(var e;;){if(A.bra=A.cursor,e=A.find_among(k,6))switch(A.ket=A.cursor,e){case 1:A.slice_from("a");continue;case 2:A.slice_from("e");continue;case 3:A.slice_from("i");continue;case 4:A.slice_from("o");continue;case 5:A.slice_from("u");continue;case 6:if(A.cursor>=A.limit)break;A.cursor++;continue}break}}function u(){return g<=A.cursor}function w(){return p<=A.cursor}function c(){return v<=A.cursor}function m(){var e;if(A.ket=A.cursor,A.find_among_b(y,13)&&(A.bra=A.cursor,(e=A.find_among_b(q,11))&&u()))switch(e){case 1:A.bra=A.cursor,A.slice_from("iendo");break;case 2:A.bra=A.cursor,A.slice_from("ando");break;case 3:A.bra=A.cursor,A.slice_from("ar");break;case 4:A.bra=A.cursor,A.slice_from("er");break;case 5:A.bra=A.cursor,A.slice_from("ir");break;case 6:A.slice_del();break;case 7:A.eq_s_b(1,"u")&&A.slice_del()}}function l(e,s){if(!c())return!0;A.slice_del(),A.ket=A.cursor;var r=A.find_among_b(e,s);return r&&(A.bra=A.cursor,1==r&&c()&&A.slice_del()),!1}function d(e){return!c()||(A.slice_del(),A.ket=A.cursor,A.eq_s_b(2,e)&&(A.bra=A.cursor,c()&&A.slice_del()),!1)}function b(){var e;if(A.ket=A.cursor,e=A.find_among_b(S,46)){switch(A.bra=A.cursor,e){case 1:if(!c())return!1;A.slice_del();break;case 2:if(d("ic"))return!1;break;case 3:if(!c())return!1;A.slice_from("log");break;case 4:if(!c())return!1;A.slice_from("u");break;case 5:if(!c())return!1;A.slice_from("ente");break;case 6:if(!w())return!1;A.slice_del(),A.ket=A.cursor,e=A.find_among_b(C,4),e&&(A.bra=A.cursor,c()&&(A.slice_del(),1==e&&(A.ket=A.cursor,A.eq_s_b(2,"at")&&(A.bra=A.cursor,c()&&A.slice_del()))));break;case 7:if(l(P,3))return!1;break;case 8:if(l(F,3))return!1;break;case 9:if(d("at"))return!1}return!0}return!1}function f(){var e,s;if(A.cursor>=g&&(s=A.limit_backward,A.limit_backward=g,A.ket=A.cursor,e=A.find_among_b(W,12),A.limit_backward=s,e)){if(A.bra=A.cursor,1==e){if(!A.eq_s_b(1,"u"))return!1;A.slice_del()}return!0}return!1}function _(){var e,s,r,n;if(A.cursor>=g&&(s=A.limit_backward,A.limit_backward=g,A.ket=A.cursor,e=A.find_among_b(L,96),A.limit_backward=s,e))switch(A.bra=A.cursor,e){case 1:r=A.limit-A.cursor,A.eq_s_b(1,"u")?(n=A.limit-A.cursor,A.eq_s_b(1,"g")?A.cursor=A.limit-n:A.cursor=A.limit-r):A.cursor=A.limit-r,A.bra=A.cursor;case 2:A.slice_del()}}function h(){var e,s;if(A.ket=A.cursor,e=A.find_among_b(z,8))switch(A.bra=A.cursor,e){case 1:u()&&A.slice_del();break;case 2:u()&&(A.slice_del(),A.ket=A.cursor,A.eq_s_b(1,"u")&&(A.bra=A.cursor,s=A.limit-A.cursor,A.eq_s_b(1,"g")&&(A.cursor=A.limit-s,u()&&A.slice_del())))}}var v,p,g,k=[new s("",-1,6),new s("á",0,1),new s("é",0,2),new s("í",0,3),new s("ó",0,4),new s("ú",0,5)],y=[new s("la",-1,-1),new s("sela",0,-1),new s("le",-1,-1),new s("me",-1,-1),new s("se",-1,-1),new s("lo",-1,-1),new s("selo",5,-1),new s("las",-1,-1),new s("selas",7,-1),new s("les",-1,-1),new s("los",-1,-1),new s("selos",10,-1),new s("nos",-1,-1)],q=[new s("ando",-1,6),new s("iendo",-1,6),new s("yendo",-1,7),new s("ándo",-1,2),new s("iéndo",-1,1),new s("ar",-1,6),new s("er",-1,6),new s("ir",-1,6),new s("ár",-1,3),new s("ér",-1,4),new s("ír",-1,5)],C=[new s("ic",-1,-1),new s("ad",-1,-1),new s("os",-1,-1),new s("iv",-1,1)],P=[new s("able",-1,1),new s("ible",-1,1),new s("ante",-1,1)],F=[new s("ic",-1,1),new s("abil",-1,1),new s("iv",-1,1)],S=[new s("ica",-1,1),new s("ancia",-1,2),new s("encia",-1,5),new s("adora",-1,2),new s("osa",-1,1),new s("ista",-1,1),new s("iva",-1,9),new s("anza",-1,1),new s("logía",-1,3),new s("idad",-1,8),new s("able",-1,1),new s("ible",-1,1),new s("ante",-1,2),new s("mente",-1,7),new s("amente",13,6),new s("ación",-1,2),new s("ución",-1,4),new s("ico",-1,1),new s("ismo",-1,1),new s("oso",-1,1),new s("amiento",-1,1),new s("imiento",-1,1),new s("ivo",-1,9),new s("ador",-1,2),new s("icas",-1,1),new s("ancias",-1,2),new s("encias",-1,5),new s("adoras",-1,2),new s("osas",-1,1),new s("istas",-1,1),new s("ivas",-1,9),new s("anzas",-1,1),new s("logías",-1,3),new s("idades",-1,8),new s("ables",-1,1),new s("ibles",-1,1),new s("aciones",-1,2),new s("uciones",-1,4),new s("adores",-1,2),new s("antes",-1,2),new s("icos",-1,1),new s("ismos",-1,1),new s("osos",-1,1),new s("amientos",-1,1),new s("imientos",-1,1),new s("ivos",-1,9)],W=[new s("ya",-1,1),new s("ye",-1,1),new s("yan",-1,1),new s("yen",-1,1),new s("yeron",-1,1),new s("yendo",-1,1),new s("yo",-1,1),new s("yas",-1,1),new s("yes",-1,1),new s("yais",-1,1),new s("yamos",-1,1),new s("yó",-1,1)],L=[new s("aba",-1,2),new s("ada",-1,2),new s("ida",-1,2),new s("ara",-1,2),new s("iera",-1,2),new s("ía",-1,2),new s("aría",5,2),new s("ería",5,2),new s("iría",5,2),new s("ad",-1,2),new s("ed",-1,2),new s("id",-1,2),new s("ase",-1,2),new s("iese",-1,2),new s("aste",-1,2),new s("iste",-1,2),new s("an",-1,2),new s("aban",16,2),new s("aran",16,2),new s("ieran",16,2),new s("ían",16,2),new s("arían",20,2),new s("erían",20,2),new s("irían",20,2),new s("en",-1,1),new s("asen",24,2),new s("iesen",24,2),new s("aron",-1,2),new s("ieron",-1,2),new s("arán",-1,2),new s("erán",-1,2),new s("irán",-1,2),new s("ado",-1,2),new s("ido",-1,2),new s("ando",-1,2),new s("iendo",-1,2),new s("ar",-1,2),new s("er",-1,2),new s("ir",-1,2),new s("as",-1,2),new s("abas",39,2),new s("adas",39,2),new s("idas",39,2),new s("aras",39,2),new s("ieras",39,2),new s("ías",39,2),new s("arías",45,2),new s("erías",45,2),new s("irías",45,2),new s("es",-1,1),new s("ases",49,2),new s("ieses",49,2),new s("abais",-1,2),new s("arais",-1,2),new s("ierais",-1,2),new s("íais",-1,2),new s("aríais",55,2),new s("eríais",55,2),new s("iríais",55,2),new s("aseis",-1,2),new s("ieseis",-1,2),new s("asteis",-1,2),new s("isteis",-1,2),new s("áis",-1,2),new s("éis",-1,1),new s("aréis",64,2),new s("eréis",64,2),new s("iréis",64,2),new s("ados",-1,2),new s("idos",-1,2),new s("amos",-1,2),new s("ábamos",70,2),new s("áramos",70,2),new s("iéramos",70,2),new s("íamos",70,2),new s("aríamos",74,2),new s("eríamos",74,2),new s("iríamos",74,2),new s("emos",-1,1),new s("aremos",78,2),new s("eremos",78,2),new s("iremos",78,2),new s("ásemos",78,2),new s("iésemos",78,2),new s("imos",-1,2),new s("arás",-1,2),new s("erás",-1,2),new s("irás",-1,2),new s("ís",-1,2),new s("ará",-1,2),new s("erá",-1,2),new s("irá",-1,2),new s("aré",-1,2),new s("eré",-1,2),new s("iré",-1,2),new s("ió",-1,2)],z=[new s("a",-1,1),new s("e",-1,2),new s("o",-1,1),new s("os",-1,1),new s("á",-1,1),new s("é",-1,2),new s("í",-1,1),new s("ó",-1,1)],x=[17,65,16,0,0,0,0,0,0,0,0,0,0,0,0,0,1,17,4,10],A=new r;this.setCurrent=function(e){A.setCurrent(e)},this.getCurrent=function(){return A.getCurrent()},this.stem=function(){var e=A.cursor;return t(),A.limit_backward=e,A.cursor=A.limit,m(),A.cursor=A.limit,b()||(A.cursor=A.limit,f()||(A.cursor=A.limit,_())),A.cursor=A.limit,h(),A.cursor=A.limit_backward,o(),!0}};return function(e){return"function"==typeof e.update?e.update(function(e){return n.setCurrent(e),n.stem(),n.getCurrent()}):(n.setCurrent(e),n.stem(),n.getCurrent())}}(),e.Pipeline.registerFunction(e.es.stemmer,"stemmer-es"),e.es.stopWordFilter=e.generateStopWordFilter("a al algo algunas algunos ante antes como con contra cual cuando de del desde donde durante e el ella ellas ellos en entre era erais eran eras eres es esa esas ese eso esos esta estaba estabais estaban estabas estad estada estadas estado estados estamos estando estar estaremos estará estarán estarás estaré estaréis estaría estaríais estaríamos estarían estarías estas este estemos esto estos estoy estuve estuviera estuvierais estuvieran estuvieras estuvieron estuviese estuvieseis estuviesen estuvieses estuvimos estuviste estuvisteis estuviéramos estuviésemos estuvo está estábamos estáis están estás esté estéis estén estés fue fuera fuerais fueran fueras fueron fuese fueseis fuesen fueses fui fuimos fuiste fuisteis fuéramos fuésemos ha habida habidas habido habidos habiendo habremos habrá habrán habrás habré habréis habría habríais habríamos habrían habrías habéis había habíais habíamos habían habías han has hasta hay haya hayamos hayan hayas hayáis he hemos hube hubiera hubierais hubieran hubieras hubieron hubiese hubieseis hubiesen hubieses hubimos hubiste hubisteis hubiéramos hubiésemos hubo la las le les lo los me mi mis mucho muchos muy más mí mía mías mío míos nada ni no nos nosotras nosotros nuestra nuestras nuestro nuestros o os otra otras otro otros para pero poco por porque que quien quienes qué se sea seamos sean seas seremos será serán serás seré seréis sería seríais seríamos serían serías seáis sido siendo sin sobre sois somos son soy su sus suya suyas suyo suyos sí también tanto te tendremos tendrá tendrán tendrás tendré tendréis tendría tendríais tendríamos tendrían tendrías tened tenemos tenga tengamos tengan tengas tengo tengáis tenida tenidas tenido tenidos teniendo tenéis tenía teníais teníamos tenían tenías ti tiene tienen tienes todo todos tu tus tuve tuviera tuvierais tuvieran tuvieras tuvieron tuviese tuvieseis tuviesen tuvieses tuvimos tuviste tuvisteis tuviéramos tuviésemos tuvo tuya tuyas tuyo tuyos tú un una uno unos vosotras vosotros vuestra vuestras vuestro vuestros y ya yo él éramos".split(" ")),e.Pipeline.registerFunction(e.es.stopWordFilter,"stopWordFilter-es")}}); \ No newline at end of file diff --git a/v0.13.6/assets/javascripts/lunr/min/lunr.fi.min.js b/v0.13.6/assets/javascripts/lunr/min/lunr.fi.min.js new file mode 100644 index 0000000000..29f5dfcea8 --- /dev/null +++ b/v0.13.6/assets/javascripts/lunr/min/lunr.fi.min.js @@ -0,0 +1,18 @@ +/*! + * Lunr languages, `Finnish` language + * https://github.com/MihaiValentin/lunr-languages + * + * Copyright 2014, Mihai Valentin + * http://www.mozilla.org/MPL/ + */ +/*! + * based on + * Snowball JavaScript Library v0.3 + * http://code.google.com/p/urim/ + * http://snowball.tartarus.org/ + * + * Copyright 2010, Oleg Mazko + * http://www.mozilla.org/MPL/ + */ + +!function(i,e){"function"==typeof define&&define.amd?define(e):"object"==typeof exports?module.exports=e():e()(i.lunr)}(this,function(){return function(i){if(void 0===i)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===i.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");i.fi=function(){this.pipeline.reset(),this.pipeline.add(i.fi.trimmer,i.fi.stopWordFilter,i.fi.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(i.fi.stemmer))},i.fi.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",i.fi.trimmer=i.trimmerSupport.generateTrimmer(i.fi.wordCharacters),i.Pipeline.registerFunction(i.fi.trimmer,"trimmer-fi"),i.fi.stemmer=function(){var e=i.stemmerSupport.Among,r=i.stemmerSupport.SnowballProgram,n=new function(){function i(){f=A.limit,d=f,n()||(f=A.cursor,n()||(d=A.cursor))}function n(){for(var i;;){if(i=A.cursor,A.in_grouping(W,97,246))break;if(A.cursor=i,i>=A.limit)return!0;A.cursor++}for(A.cursor=i;!A.out_grouping(W,97,246);){if(A.cursor>=A.limit)return!0;A.cursor++}return!1}function t(){return d<=A.cursor}function s(){var i,e;if(A.cursor>=f)if(e=A.limit_backward,A.limit_backward=f,A.ket=A.cursor,i=A.find_among_b(h,10)){switch(A.bra=A.cursor,A.limit_backward=e,i){case 1:if(!A.in_grouping_b(x,97,246))return;break;case 2:if(!t())return}A.slice_del()}else A.limit_backward=e}function o(){var i,e,r;if(A.cursor>=f)if(e=A.limit_backward,A.limit_backward=f,A.ket=A.cursor,i=A.find_among_b(v,9))switch(A.bra=A.cursor,A.limit_backward=e,i){case 1:r=A.limit-A.cursor,A.eq_s_b(1,"k")||(A.cursor=A.limit-r,A.slice_del());break;case 2:A.slice_del(),A.ket=A.cursor,A.eq_s_b(3,"kse")&&(A.bra=A.cursor,A.slice_from("ksi"));break;case 3:A.slice_del();break;case 4:A.find_among_b(p,6)&&A.slice_del();break;case 5:A.find_among_b(g,6)&&A.slice_del();break;case 6:A.find_among_b(j,2)&&A.slice_del()}else A.limit_backward=e}function l(){return A.find_among_b(q,7)}function a(){return A.eq_s_b(1,"i")&&A.in_grouping_b(L,97,246)}function u(){var i,e,r;if(A.cursor>=f)if(e=A.limit_backward,A.limit_backward=f,A.ket=A.cursor,i=A.find_among_b(C,30)){switch(A.bra=A.cursor,A.limit_backward=e,i){case 1:if(!A.eq_s_b(1,"a"))return;break;case 2:case 9:if(!A.eq_s_b(1,"e"))return;break;case 3:if(!A.eq_s_b(1,"i"))return;break;case 4:if(!A.eq_s_b(1,"o"))return;break;case 5:if(!A.eq_s_b(1,"ä"))return;break;case 6:if(!A.eq_s_b(1,"ö"))return;break;case 7:if(r=A.limit-A.cursor,!l()&&(A.cursor=A.limit-r,!A.eq_s_b(2,"ie"))){A.cursor=A.limit-r;break}if(A.cursor=A.limit-r,A.cursor<=A.limit_backward){A.cursor=A.limit-r;break}A.cursor--,A.bra=A.cursor;break;case 8:if(!A.in_grouping_b(W,97,246)||!A.out_grouping_b(W,97,246))return}A.slice_del(),k=!0}else A.limit_backward=e}function c(){var i,e,r;if(A.cursor>=d)if(e=A.limit_backward,A.limit_backward=d,A.ket=A.cursor,i=A.find_among_b(P,14)){if(A.bra=A.cursor,A.limit_backward=e,1==i){if(r=A.limit-A.cursor,A.eq_s_b(2,"po"))return;A.cursor=A.limit-r}A.slice_del()}else A.limit_backward=e}function m(){var i;A.cursor>=f&&(i=A.limit_backward,A.limit_backward=f,A.ket=A.cursor,A.find_among_b(F,2)?(A.bra=A.cursor,A.limit_backward=i,A.slice_del()):A.limit_backward=i)}function w(){var i,e,r,n,t,s;if(A.cursor>=f){if(e=A.limit_backward,A.limit_backward=f,A.ket=A.cursor,A.eq_s_b(1,"t")&&(A.bra=A.cursor,r=A.limit-A.cursor,A.in_grouping_b(W,97,246)&&(A.cursor=A.limit-r,A.slice_del(),A.limit_backward=e,n=A.limit-A.cursor,A.cursor>=d&&(A.cursor=d,t=A.limit_backward,A.limit_backward=A.cursor,A.cursor=A.limit-n,A.ket=A.cursor,i=A.find_among_b(S,2))))){if(A.bra=A.cursor,A.limit_backward=t,1==i){if(s=A.limit-A.cursor,A.eq_s_b(2,"po"))return;A.cursor=A.limit-s}return void A.slice_del()}A.limit_backward=e}}function _(){var i,e,r,n;if(A.cursor>=f){for(i=A.limit_backward,A.limit_backward=f,e=A.limit-A.cursor,l()&&(A.cursor=A.limit-e,A.ket=A.cursor,A.cursor>A.limit_backward&&(A.cursor--,A.bra=A.cursor,A.slice_del())),A.cursor=A.limit-e,A.ket=A.cursor,A.in_grouping_b(y,97,228)&&(A.bra=A.cursor,A.out_grouping_b(W,97,246)&&A.slice_del()),A.cursor=A.limit-e,A.ket=A.cursor,A.eq_s_b(1,"j")&&(A.bra=A.cursor,r=A.limit-A.cursor,A.eq_s_b(1,"o")?A.slice_del():(A.cursor=A.limit-r,A.eq_s_b(1,"u")&&A.slice_del())),A.cursor=A.limit-e,A.ket=A.cursor,A.eq_s_b(1,"o")&&(A.bra=A.cursor,A.eq_s_b(1,"j")&&A.slice_del()),A.cursor=A.limit-e,A.limit_backward=i;;){if(n=A.limit-A.cursor,A.out_grouping_b(W,97,246)){A.cursor=A.limit-n;break}if(A.cursor=A.limit-n,A.cursor<=A.limit_backward)return;A.cursor--}A.ket=A.cursor,A.cursor>A.limit_backward&&(A.cursor--,A.bra=A.cursor,b=A.slice_to(),A.eq_v_b(b)&&A.slice_del())}}var k,b,d,f,h=[new e("pa",-1,1),new e("sti",-1,2),new e("kaan",-1,1),new e("han",-1,1),new e("kin",-1,1),new e("hän",-1,1),new e("kään",-1,1),new e("ko",-1,1),new e("pä",-1,1),new e("kö",-1,1)],p=[new e("lla",-1,-1),new e("na",-1,-1),new e("ssa",-1,-1),new e("ta",-1,-1),new e("lta",3,-1),new e("sta",3,-1)],g=[new e("llä",-1,-1),new e("nä",-1,-1),new e("ssä",-1,-1),new e("tä",-1,-1),new e("ltä",3,-1),new e("stä",3,-1)],j=[new e("lle",-1,-1),new e("ine",-1,-1)],v=[new e("nsa",-1,3),new e("mme",-1,3),new e("nne",-1,3),new e("ni",-1,2),new e("si",-1,1),new e("an",-1,4),new e("en",-1,6),new e("än",-1,5),new e("nsä",-1,3)],q=[new e("aa",-1,-1),new e("ee",-1,-1),new e("ii",-1,-1),new e("oo",-1,-1),new e("uu",-1,-1),new e("ää",-1,-1),new e("öö",-1,-1)],C=[new e("a",-1,8),new e("lla",0,-1),new e("na",0,-1),new e("ssa",0,-1),new e("ta",0,-1),new e("lta",4,-1),new e("sta",4,-1),new e("tta",4,9),new e("lle",-1,-1),new e("ine",-1,-1),new e("ksi",-1,-1),new e("n",-1,7),new e("han",11,1),new e("den",11,-1,a),new e("seen",11,-1,l),new e("hen",11,2),new e("tten",11,-1,a),new e("hin",11,3),new e("siin",11,-1,a),new e("hon",11,4),new e("hän",11,5),new e("hön",11,6),new e("ä",-1,8),new e("llä",22,-1),new e("nä",22,-1),new e("ssä",22,-1),new e("tä",22,-1),new e("ltä",26,-1),new e("stä",26,-1),new e("ttä",26,9)],P=[new e("eja",-1,-1),new e("mma",-1,1),new e("imma",1,-1),new e("mpa",-1,1),new e("impa",3,-1),new e("mmi",-1,1),new e("immi",5,-1),new e("mpi",-1,1),new e("impi",7,-1),new e("ejä",-1,-1),new e("mmä",-1,1),new e("immä",10,-1),new e("mpä",-1,1),new e("impä",12,-1)],F=[new e("i",-1,-1),new e("j",-1,-1)],S=[new e("mma",-1,1),new e("imma",0,-1)],y=[17,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8],W=[17,65,16,1,0,0,0,0,0,0,0,0,0,0,0,0,8,0,32],L=[17,65,16,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,32],x=[17,97,24,1,0,0,0,0,0,0,0,0,0,0,0,0,8,0,32],A=new r;this.setCurrent=function(i){A.setCurrent(i)},this.getCurrent=function(){return A.getCurrent()},this.stem=function(){var e=A.cursor;return i(),k=!1,A.limit_backward=e,A.cursor=A.limit,s(),A.cursor=A.limit,o(),A.cursor=A.limit,u(),A.cursor=A.limit,c(),A.cursor=A.limit,k?(m(),A.cursor=A.limit):(A.cursor=A.limit,w(),A.cursor=A.limit),_(),!0}};return function(i){return"function"==typeof i.update?i.update(function(i){return n.setCurrent(i),n.stem(),n.getCurrent()}):(n.setCurrent(i),n.stem(),n.getCurrent())}}(),i.Pipeline.registerFunction(i.fi.stemmer,"stemmer-fi"),i.fi.stopWordFilter=i.generateStopWordFilter("ei eivät emme en et ette että he heidän heidät heihin heille heillä heiltä heissä heistä heitä hän häneen hänelle hänellä häneltä hänen hänessä hänestä hänet häntä itse ja johon joiden joihin joiksi joilla joille joilta joina joissa joista joita joka joksi jolla jolle jolta jona jonka jos jossa josta jota jotka kanssa keiden keihin keiksi keille keillä keiltä keinä keissä keistä keitä keneen keneksi kenelle kenellä keneltä kenen kenenä kenessä kenestä kenet ketkä ketkä ketä koska kuin kuka kun me meidän meidät meihin meille meillä meiltä meissä meistä meitä mihin miksi mikä mille millä miltä minkä minkä minua minulla minulle minulta minun minussa minusta minut minuun minä minä missä mistä mitkä mitä mukaan mutta ne niiden niihin niiksi niille niillä niiltä niin niin niinä niissä niistä niitä noiden noihin noiksi noilla noille noilta noin noina noissa noista noita nuo nyt näiden näihin näiksi näille näillä näiltä näinä näissä näistä näitä nämä ole olemme olen olet olette oli olimme olin olisi olisimme olisin olisit olisitte olisivat olit olitte olivat olla olleet ollut on ovat poikki se sekä sen siihen siinä siitä siksi sille sillä sillä siltä sinua sinulla sinulle sinulta sinun sinussa sinusta sinut sinuun sinä sinä sitä tai te teidän teidät teihin teille teillä teiltä teissä teistä teitä tuo tuohon tuoksi tuolla tuolle tuolta tuon tuona tuossa tuosta tuota tähän täksi tälle tällä tältä tämä tämän tänä tässä tästä tätä vaan vai vaikka yli".split(" ")),i.Pipeline.registerFunction(i.fi.stopWordFilter,"stopWordFilter-fi")}}); \ No newline at end of file diff --git a/v0.13.6/assets/javascripts/lunr/min/lunr.fr.min.js b/v0.13.6/assets/javascripts/lunr/min/lunr.fr.min.js new file mode 100644 index 0000000000..68cd0094ae --- /dev/null +++ b/v0.13.6/assets/javascripts/lunr/min/lunr.fr.min.js @@ -0,0 +1,18 @@ +/*! + * Lunr languages, `French` language + * https://github.com/MihaiValentin/lunr-languages + * + * Copyright 2014, Mihai Valentin + * http://www.mozilla.org/MPL/ + */ +/*! + * based on + * Snowball JavaScript Library v0.3 + * http://code.google.com/p/urim/ + * http://snowball.tartarus.org/ + * + * Copyright 2010, Oleg Mazko + * http://www.mozilla.org/MPL/ + */ + +!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.fr=function(){this.pipeline.reset(),this.pipeline.add(e.fr.trimmer,e.fr.stopWordFilter,e.fr.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.fr.stemmer))},e.fr.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",e.fr.trimmer=e.trimmerSupport.generateTrimmer(e.fr.wordCharacters),e.Pipeline.registerFunction(e.fr.trimmer,"trimmer-fr"),e.fr.stemmer=function(){var r=e.stemmerSupport.Among,s=e.stemmerSupport.SnowballProgram,i=new function(){function e(e,r,s){return!(!W.eq_s(1,e)||(W.ket=W.cursor,!W.in_grouping(F,97,251)))&&(W.slice_from(r),W.cursor=s,!0)}function i(e,r,s){return!!W.eq_s(1,e)&&(W.ket=W.cursor,W.slice_from(r),W.cursor=s,!0)}function n(){for(var r,s;;){if(r=W.cursor,W.in_grouping(F,97,251)){if(W.bra=W.cursor,s=W.cursor,e("u","U",r))continue;if(W.cursor=s,e("i","I",r))continue;if(W.cursor=s,i("y","Y",r))continue}if(W.cursor=r,W.bra=r,!e("y","Y",r)){if(W.cursor=r,W.eq_s(1,"q")&&(W.bra=W.cursor,i("u","U",r)))continue;if(W.cursor=r,r>=W.limit)return;W.cursor++}}}function t(){for(;!W.in_grouping(F,97,251);){if(W.cursor>=W.limit)return!0;W.cursor++}for(;!W.out_grouping(F,97,251);){if(W.cursor>=W.limit)return!0;W.cursor++}return!1}function u(){var e=W.cursor;if(q=W.limit,g=q,p=q,W.in_grouping(F,97,251)&&W.in_grouping(F,97,251)&&W.cursor=W.limit){W.cursor=q;break}W.cursor++}while(!W.in_grouping(F,97,251))}q=W.cursor,W.cursor=e,t()||(g=W.cursor,t()||(p=W.cursor))}function o(){for(var e,r;;){if(r=W.cursor,W.bra=r,!(e=W.find_among(h,4)))break;switch(W.ket=W.cursor,e){case 1:W.slice_from("i");break;case 2:W.slice_from("u");break;case 3:W.slice_from("y");break;case 4:if(W.cursor>=W.limit)return;W.cursor++}}}function c(){return q<=W.cursor}function a(){return g<=W.cursor}function l(){return p<=W.cursor}function w(){var e,r;if(W.ket=W.cursor,e=W.find_among_b(C,43)){switch(W.bra=W.cursor,e){case 1:if(!l())return!1;W.slice_del();break;case 2:if(!l())return!1;W.slice_del(),W.ket=W.cursor,W.eq_s_b(2,"ic")&&(W.bra=W.cursor,l()?W.slice_del():W.slice_from("iqU"));break;case 3:if(!l())return!1;W.slice_from("log");break;case 4:if(!l())return!1;W.slice_from("u");break;case 5:if(!l())return!1;W.slice_from("ent");break;case 6:if(!c())return!1;if(W.slice_del(),W.ket=W.cursor,e=W.find_among_b(z,6))switch(W.bra=W.cursor,e){case 1:l()&&(W.slice_del(),W.ket=W.cursor,W.eq_s_b(2,"at")&&(W.bra=W.cursor,l()&&W.slice_del()));break;case 2:l()?W.slice_del():a()&&W.slice_from("eux");break;case 3:l()&&W.slice_del();break;case 4:c()&&W.slice_from("i")}break;case 7:if(!l())return!1;if(W.slice_del(),W.ket=W.cursor,e=W.find_among_b(y,3))switch(W.bra=W.cursor,e){case 1:l()?W.slice_del():W.slice_from("abl");break;case 2:l()?W.slice_del():W.slice_from("iqU");break;case 3:l()&&W.slice_del()}break;case 8:if(!l())return!1;if(W.slice_del(),W.ket=W.cursor,W.eq_s_b(2,"at")&&(W.bra=W.cursor,l()&&(W.slice_del(),W.ket=W.cursor,W.eq_s_b(2,"ic")))){W.bra=W.cursor,l()?W.slice_del():W.slice_from("iqU");break}break;case 9:W.slice_from("eau");break;case 10:if(!a())return!1;W.slice_from("al");break;case 11:if(l())W.slice_del();else{if(!a())return!1;W.slice_from("eux")}break;case 12:if(!a()||!W.out_grouping_b(F,97,251))return!1;W.slice_del();break;case 13:return c()&&W.slice_from("ant"),!1;case 14:return c()&&W.slice_from("ent"),!1;case 15:return r=W.limit-W.cursor,W.in_grouping_b(F,97,251)&&c()&&(W.cursor=W.limit-r,W.slice_del()),!1}return!0}return!1}function f(){var e,r;if(W.cursor=q){if(s=W.limit_backward,W.limit_backward=q,W.ket=W.cursor,e=W.find_among_b(P,7))switch(W.bra=W.cursor,e){case 1:if(l()){if(i=W.limit-W.cursor,!W.eq_s_b(1,"s")&&(W.cursor=W.limit-i,!W.eq_s_b(1,"t")))break;W.slice_del()}break;case 2:W.slice_from("i");break;case 3:W.slice_del();break;case 4:W.eq_s_b(2,"gu")&&W.slice_del()}W.limit_backward=s}}function b(){var e=W.limit-W.cursor;W.find_among_b(U,5)&&(W.cursor=W.limit-e,W.ket=W.cursor,W.cursor>W.limit_backward&&(W.cursor--,W.bra=W.cursor,W.slice_del()))}function d(){for(var e,r=1;W.out_grouping_b(F,97,251);)r--;if(r<=0){if(W.ket=W.cursor,e=W.limit-W.cursor,!W.eq_s_b(1,"é")&&(W.cursor=W.limit-e,!W.eq_s_b(1,"è")))return;W.bra=W.cursor,W.slice_from("e")}}function k(){if(!w()&&(W.cursor=W.limit,!f()&&(W.cursor=W.limit,!m())))return W.cursor=W.limit,void _();W.cursor=W.limit,W.ket=W.cursor,W.eq_s_b(1,"Y")?(W.bra=W.cursor,W.slice_from("i")):(W.cursor=W.limit,W.eq_s_b(1,"ç")&&(W.bra=W.cursor,W.slice_from("c")))}var p,g,q,v=[new r("col",-1,-1),new r("par",-1,-1),new r("tap",-1,-1)],h=[new r("",-1,4),new r("I",0,1),new r("U",0,2),new r("Y",0,3)],z=[new r("iqU",-1,3),new r("abl",-1,3),new r("Ièr",-1,4),new r("ièr",-1,4),new r("eus",-1,2),new r("iv",-1,1)],y=[new r("ic",-1,2),new r("abil",-1,1),new r("iv",-1,3)],C=[new r("iqUe",-1,1),new r("atrice",-1,2),new r("ance",-1,1),new r("ence",-1,5),new r("logie",-1,3),new r("able",-1,1),new r("isme",-1,1),new r("euse",-1,11),new r("iste",-1,1),new r("ive",-1,8),new r("if",-1,8),new r("usion",-1,4),new r("ation",-1,2),new r("ution",-1,4),new r("ateur",-1,2),new r("iqUes",-1,1),new r("atrices",-1,2),new r("ances",-1,1),new r("ences",-1,5),new r("logies",-1,3),new r("ables",-1,1),new r("ismes",-1,1),new r("euses",-1,11),new r("istes",-1,1),new r("ives",-1,8),new r("ifs",-1,8),new r("usions",-1,4),new r("ations",-1,2),new r("utions",-1,4),new r("ateurs",-1,2),new r("ments",-1,15),new r("ements",30,6),new r("issements",31,12),new r("ités",-1,7),new r("ment",-1,15),new r("ement",34,6),new r("issement",35,12),new r("amment",34,13),new r("emment",34,14),new r("aux",-1,10),new r("eaux",39,9),new r("eux",-1,1),new r("ité",-1,7)],x=[new r("ira",-1,1),new r("ie",-1,1),new r("isse",-1,1),new r("issante",-1,1),new r("i",-1,1),new r("irai",4,1),new r("ir",-1,1),new r("iras",-1,1),new r("ies",-1,1),new r("îmes",-1,1),new r("isses",-1,1),new r("issantes",-1,1),new r("îtes",-1,1),new r("is",-1,1),new r("irais",13,1),new r("issais",13,1),new r("irions",-1,1),new r("issions",-1,1),new r("irons",-1,1),new r("issons",-1,1),new r("issants",-1,1),new r("it",-1,1),new r("irait",21,1),new r("issait",21,1),new r("issant",-1,1),new r("iraIent",-1,1),new r("issaIent",-1,1),new r("irent",-1,1),new r("issent",-1,1),new r("iront",-1,1),new r("ît",-1,1),new r("iriez",-1,1),new r("issiez",-1,1),new r("irez",-1,1),new r("issez",-1,1)],I=[new r("a",-1,3),new r("era",0,2),new r("asse",-1,3),new r("ante",-1,3),new r("ée",-1,2),new r("ai",-1,3),new r("erai",5,2),new r("er",-1,2),new r("as",-1,3),new r("eras",8,2),new r("âmes",-1,3),new r("asses",-1,3),new r("antes",-1,3),new r("âtes",-1,3),new r("ées",-1,2),new r("ais",-1,3),new r("erais",15,2),new r("ions",-1,1),new r("erions",17,2),new r("assions",17,3),new r("erons",-1,2),new r("ants",-1,3),new r("és",-1,2),new r("ait",-1,3),new r("erait",23,2),new r("ant",-1,3),new r("aIent",-1,3),new r("eraIent",26,2),new r("èrent",-1,2),new r("assent",-1,3),new r("eront",-1,2),new r("ât",-1,3),new r("ez",-1,2),new r("iez",32,2),new r("eriez",33,2),new r("assiez",33,3),new r("erez",32,2),new r("é",-1,2)],P=[new r("e",-1,3),new r("Ière",0,2),new r("ière",0,2),new r("ion",-1,1),new r("Ier",-1,2),new r("ier",-1,2),new r("ë",-1,4)],U=[new r("ell",-1,-1),new r("eill",-1,-1),new r("enn",-1,-1),new r("onn",-1,-1),new r("ett",-1,-1)],F=[17,65,16,1,0,0,0,0,0,0,0,0,0,0,0,128,130,103,8,5],S=[1,65,20,0,0,0,0,0,0,0,0,0,0,0,0,0,128],W=new s;this.setCurrent=function(e){W.setCurrent(e)},this.getCurrent=function(){return W.getCurrent()},this.stem=function(){var e=W.cursor;return n(),W.cursor=e,u(),W.limit_backward=e,W.cursor=W.limit,k(),W.cursor=W.limit,b(),W.cursor=W.limit,d(),W.cursor=W.limit_backward,o(),!0}};return function(e){return"function"==typeof e.update?e.update(function(e){return i.setCurrent(e),i.stem(),i.getCurrent()}):(i.setCurrent(e),i.stem(),i.getCurrent())}}(),e.Pipeline.registerFunction(e.fr.stemmer,"stemmer-fr"),e.fr.stopWordFilter=e.generateStopWordFilter("ai aie aient aies ait as au aura aurai auraient aurais aurait auras aurez auriez aurions aurons auront aux avaient avais avait avec avez aviez avions avons ayant ayez ayons c ce ceci celà ces cet cette d dans de des du elle en es est et eu eue eues eurent eus eusse eussent eusses eussiez eussions eut eux eûmes eût eûtes furent fus fusse fussent fusses fussiez fussions fut fûmes fût fûtes ici il ils j je l la le les leur leurs lui m ma mais me mes moi mon même n ne nos notre nous on ont ou par pas pour qu que quel quelle quelles quels qui s sa sans se sera serai seraient serais serait seras serez seriez serions serons seront ses soi soient sois soit sommes son sont soyez soyons suis sur t ta te tes toi ton tu un une vos votre vous y à étaient étais était étant étiez étions été étée étées étés êtes".split(" ")),e.Pipeline.registerFunction(e.fr.stopWordFilter,"stopWordFilter-fr")}}); \ No newline at end of file diff --git a/v0.13.6/assets/javascripts/lunr/min/lunr.hi.min.js b/v0.13.6/assets/javascripts/lunr/min/lunr.hi.min.js new file mode 100644 index 0000000000..7dbc41402c --- /dev/null +++ b/v0.13.6/assets/javascripts/lunr/min/lunr.hi.min.js @@ -0,0 +1 @@ +!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.hi=function(){this.pipeline.reset(),this.pipeline.add(e.hi.trimmer,e.hi.stopWordFilter,e.hi.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.hi.stemmer))},e.hi.wordCharacters="ऀ-ःऄ-एऐ-टठ-यर-िी-ॏॐ-य़ॠ-९॰-ॿa-zA-Za-zA-Z0-90-9",e.hi.trimmer=e.trimmerSupport.generateTrimmer(e.hi.wordCharacters),e.Pipeline.registerFunction(e.hi.trimmer,"trimmer-hi"),e.hi.stopWordFilter=e.generateStopWordFilter("अत अपना अपनी अपने अभी अंदर आदि आप इत्यादि इन इनका इन्हीं इन्हें इन्हों इस इसका इसकी इसके इसमें इसी इसे उन उनका उनकी उनके उनको उन्हीं उन्हें उन्हों उस उसके उसी उसे एक एवं एस ऐसे और कई कर करता करते करना करने करें कहते कहा का काफ़ी कि कितना किन्हें किन्हों किया किर किस किसी किसे की कुछ कुल के को कोई कौन कौनसा गया घर जब जहाँ जा जितना जिन जिन्हें जिन्हों जिस जिसे जीधर जैसा जैसे जो तक तब तरह तिन तिन्हें तिन्हों तिस तिसे तो था थी थे दबारा दिया दुसरा दूसरे दो द्वारा न नके नहीं ना निहायत नीचे ने पर पहले पूरा पे फिर बनी बही बहुत बाद बाला बिलकुल भी भीतर मगर मानो मे में यदि यह यहाँ यही या यिह ये रखें रहा रहे ऱ्वासा लिए लिये लेकिन व वग़ैरह वर्ग वह वहाँ वहीं वाले वुह वे वो सकता सकते सबसे सभी साथ साबुत साभ सारा से सो संग ही हुआ हुई हुए है हैं हो होता होती होते होना होने".split(" ")),e.hi.stemmer=function(){return function(e){return"function"==typeof e.update?e.update(function(e){return e}):e}}();var r=e.wordcut;r.init(),e.hi.tokenizer=function(i){if(!arguments.length||null==i||void 0==i)return[];if(Array.isArray(i))return i.map(function(r){return isLunr2?new e.Token(r.toLowerCase()):r.toLowerCase()});var t=i.toString().toLowerCase().replace(/^\s+/,"");return r.cut(t).split("|")},e.Pipeline.registerFunction(e.hi.stemmer,"stemmer-hi"),e.Pipeline.registerFunction(e.hi.stopWordFilter,"stopWordFilter-hi")}}); \ No newline at end of file diff --git a/v0.13.6/assets/javascripts/lunr/min/lunr.hu.min.js b/v0.13.6/assets/javascripts/lunr/min/lunr.hu.min.js new file mode 100644 index 0000000000..ed9d909f73 --- /dev/null +++ b/v0.13.6/assets/javascripts/lunr/min/lunr.hu.min.js @@ -0,0 +1,18 @@ +/*! + * Lunr languages, `Hungarian` language + * https://github.com/MihaiValentin/lunr-languages + * + * Copyright 2014, Mihai Valentin + * http://www.mozilla.org/MPL/ + */ +/*! + * based on + * Snowball JavaScript Library v0.3 + * http://code.google.com/p/urim/ + * http://snowball.tartarus.org/ + * + * Copyright 2010, Oleg Mazko + * http://www.mozilla.org/MPL/ + */ + +!function(e,n){"function"==typeof define&&define.amd?define(n):"object"==typeof exports?module.exports=n():n()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.hu=function(){this.pipeline.reset(),this.pipeline.add(e.hu.trimmer,e.hu.stopWordFilter,e.hu.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.hu.stemmer))},e.hu.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",e.hu.trimmer=e.trimmerSupport.generateTrimmer(e.hu.wordCharacters),e.Pipeline.registerFunction(e.hu.trimmer,"trimmer-hu"),e.hu.stemmer=function(){var n=e.stemmerSupport.Among,r=e.stemmerSupport.SnowballProgram,i=new function(){function e(){var e,n=L.cursor;if(d=L.limit,L.in_grouping(W,97,252))for(;;){if(e=L.cursor,L.out_grouping(W,97,252))return L.cursor=e,L.find_among(g,8)||(L.cursor=e,e=L.limit)return void(d=e);L.cursor++}if(L.cursor=n,L.out_grouping(W,97,252)){for(;!L.in_grouping(W,97,252);){if(L.cursor>=L.limit)return;L.cursor++}d=L.cursor}}function i(){return d<=L.cursor}function a(){var e;if(L.ket=L.cursor,(e=L.find_among_b(h,2))&&(L.bra=L.cursor,i()))switch(e){case 1:L.slice_from("a");break;case 2:L.slice_from("e")}}function t(){var e=L.limit-L.cursor;return!!L.find_among_b(p,23)&&(L.cursor=L.limit-e,!0)}function s(){if(L.cursor>L.limit_backward){L.cursor--,L.ket=L.cursor;var e=L.cursor-1;L.limit_backward<=e&&e<=L.limit&&(L.cursor=e,L.bra=e,L.slice_del())}}function c(){var e;if(L.ket=L.cursor,(e=L.find_among_b(_,2))&&(L.bra=L.cursor,i())){if((1==e||2==e)&&!t())return;L.slice_del(),s()}}function o(){L.ket=L.cursor,L.find_among_b(v,44)&&(L.bra=L.cursor,i()&&(L.slice_del(),a()))}function w(){var e;if(L.ket=L.cursor,(e=L.find_among_b(z,3))&&(L.bra=L.cursor,i()))switch(e){case 1:L.slice_from("e");break;case 2:case 3:L.slice_from("a")}}function l(){var e;if(L.ket=L.cursor,(e=L.find_among_b(y,6))&&(L.bra=L.cursor,i()))switch(e){case 1:case 2:L.slice_del();break;case 3:L.slice_from("a");break;case 4:L.slice_from("e")}}function u(){var e;if(L.ket=L.cursor,(e=L.find_among_b(j,2))&&(L.bra=L.cursor,i())){if((1==e||2==e)&&!t())return;L.slice_del(),s()}}function m(){var e;if(L.ket=L.cursor,(e=L.find_among_b(C,7))&&(L.bra=L.cursor,i()))switch(e){case 1:L.slice_from("a");break;case 2:L.slice_from("e");break;case 3:case 4:case 5:case 6:case 7:L.slice_del()}}function k(){var e;if(L.ket=L.cursor,(e=L.find_among_b(P,12))&&(L.bra=L.cursor,i()))switch(e){case 1:case 4:case 7:case 9:L.slice_del();break;case 2:case 5:case 8:L.slice_from("e");break;case 3:case 6:L.slice_from("a")}}function f(){var e;if(L.ket=L.cursor,(e=L.find_among_b(F,31))&&(L.bra=L.cursor,i()))switch(e){case 1:case 4:case 7:case 8:case 9:case 12:case 13:case 16:case 17:case 18:L.slice_del();break;case 2:case 5:case 10:case 14:case 19:L.slice_from("a");break;case 3:case 6:case 11:case 15:case 20:L.slice_from("e")}}function b(){var e;if(L.ket=L.cursor,(e=L.find_among_b(S,42))&&(L.bra=L.cursor,i()))switch(e){case 1:case 4:case 5:case 6:case 9:case 10:case 11:case 14:case 15:case 16:case 17:case 20:case 21:case 24:case 25:case 26:case 29:L.slice_del();break;case 2:case 7:case 12:case 18:case 22:case 27:L.slice_from("a");break;case 3:case 8:case 13:case 19:case 23:case 28:L.slice_from("e")}}var d,g=[new n("cs",-1,-1),new n("dzs",-1,-1),new n("gy",-1,-1),new n("ly",-1,-1),new n("ny",-1,-1),new n("sz",-1,-1),new n("ty",-1,-1),new n("zs",-1,-1)],h=[new n("á",-1,1),new n("é",-1,2)],p=[new n("bb",-1,-1),new n("cc",-1,-1),new n("dd",-1,-1),new n("ff",-1,-1),new n("gg",-1,-1),new n("jj",-1,-1),new n("kk",-1,-1),new n("ll",-1,-1),new n("mm",-1,-1),new n("nn",-1,-1),new n("pp",-1,-1),new n("rr",-1,-1),new n("ccs",-1,-1),new n("ss",-1,-1),new n("zzs",-1,-1),new n("tt",-1,-1),new n("vv",-1,-1),new n("ggy",-1,-1),new n("lly",-1,-1),new n("nny",-1,-1),new n("tty",-1,-1),new n("ssz",-1,-1),new n("zz",-1,-1)],_=[new n("al",-1,1),new n("el",-1,2)],v=[new n("ba",-1,-1),new n("ra",-1,-1),new n("be",-1,-1),new n("re",-1,-1),new n("ig",-1,-1),new n("nak",-1,-1),new n("nek",-1,-1),new n("val",-1,-1),new n("vel",-1,-1),new n("ul",-1,-1),new n("nál",-1,-1),new n("nél",-1,-1),new n("ból",-1,-1),new n("ról",-1,-1),new n("tól",-1,-1),new n("bõl",-1,-1),new n("rõl",-1,-1),new n("tõl",-1,-1),new n("ül",-1,-1),new n("n",-1,-1),new n("an",19,-1),new n("ban",20,-1),new n("en",19,-1),new n("ben",22,-1),new n("képpen",22,-1),new n("on",19,-1),new n("ön",19,-1),new n("képp",-1,-1),new n("kor",-1,-1),new n("t",-1,-1),new n("at",29,-1),new n("et",29,-1),new n("ként",29,-1),new n("anként",32,-1),new n("enként",32,-1),new n("onként",32,-1),new n("ot",29,-1),new n("ért",29,-1),new n("öt",29,-1),new n("hez",-1,-1),new n("hoz",-1,-1),new n("höz",-1,-1),new n("vá",-1,-1),new n("vé",-1,-1)],z=[new n("án",-1,2),new n("én",-1,1),new n("ánként",-1,3)],y=[new n("stul",-1,2),new n("astul",0,1),new n("ástul",0,3),new n("stül",-1,2),new n("estül",3,1),new n("éstül",3,4)],j=[new n("á",-1,1),new n("é",-1,2)],C=[new n("k",-1,7),new n("ak",0,4),new n("ek",0,6),new n("ok",0,5),new n("ák",0,1),new n("ék",0,2),new n("ök",0,3)],P=[new n("éi",-1,7),new n("áéi",0,6),new n("ééi",0,5),new n("é",-1,9),new n("ké",3,4),new n("aké",4,1),new n("eké",4,1),new n("oké",4,1),new n("áké",4,3),new n("éké",4,2),new n("öké",4,1),new n("éé",3,8)],F=[new n("a",-1,18),new n("ja",0,17),new n("d",-1,16),new n("ad",2,13),new n("ed",2,13),new n("od",2,13),new n("ád",2,14),new n("éd",2,15),new n("öd",2,13),new n("e",-1,18),new n("je",9,17),new n("nk",-1,4),new n("unk",11,1),new n("ánk",11,2),new n("énk",11,3),new n("ünk",11,1),new n("uk",-1,8),new n("juk",16,7),new n("ájuk",17,5),new n("ük",-1,8),new n("jük",19,7),new n("éjük",20,6),new n("m",-1,12),new n("am",22,9),new n("em",22,9),new n("om",22,9),new n("ám",22,10),new n("ém",22,11),new n("o",-1,18),new n("á",-1,19),new n("é",-1,20)],S=[new n("id",-1,10),new n("aid",0,9),new n("jaid",1,6),new n("eid",0,9),new n("jeid",3,6),new n("áid",0,7),new n("éid",0,8),new n("i",-1,15),new n("ai",7,14),new n("jai",8,11),new n("ei",7,14),new n("jei",10,11),new n("ái",7,12),new n("éi",7,13),new n("itek",-1,24),new n("eitek",14,21),new n("jeitek",15,20),new n("éitek",14,23),new n("ik",-1,29),new n("aik",18,26),new n("jaik",19,25),new n("eik",18,26),new n("jeik",21,25),new n("áik",18,27),new n("éik",18,28),new n("ink",-1,20),new n("aink",25,17),new n("jaink",26,16),new n("eink",25,17),new n("jeink",28,16),new n("áink",25,18),new n("éink",25,19),new n("aitok",-1,21),new n("jaitok",32,20),new n("áitok",-1,22),new n("im",-1,5),new n("aim",35,4),new n("jaim",36,1),new n("eim",35,4),new n("jeim",38,1),new n("áim",35,2),new n("éim",35,3)],W=[17,65,16,0,0,0,0,0,0,0,0,0,0,0,0,0,1,17,52,14],L=new r;this.setCurrent=function(e){L.setCurrent(e)},this.getCurrent=function(){return L.getCurrent()},this.stem=function(){var n=L.cursor;return e(),L.limit_backward=n,L.cursor=L.limit,c(),L.cursor=L.limit,o(),L.cursor=L.limit,w(),L.cursor=L.limit,l(),L.cursor=L.limit,u(),L.cursor=L.limit,k(),L.cursor=L.limit,f(),L.cursor=L.limit,b(),L.cursor=L.limit,m(),!0}};return function(e){return"function"==typeof e.update?e.update(function(e){return i.setCurrent(e),i.stem(),i.getCurrent()}):(i.setCurrent(e),i.stem(),i.getCurrent())}}(),e.Pipeline.registerFunction(e.hu.stemmer,"stemmer-hu"),e.hu.stopWordFilter=e.generateStopWordFilter("a abban ahhoz ahogy ahol aki akik akkor alatt amely amelyek amelyekben amelyeket amelyet amelynek ami amikor amit amolyan amíg annak arra arról az azok azon azonban azt aztán azután azzal azért be belül benne bár cikk cikkek cikkeket csak de e ebben eddig egy egyes egyetlen egyik egyre egyéb egész ehhez ekkor el ellen elsõ elég elõ elõször elõtt emilyen ennek erre ez ezek ezen ezt ezzel ezért fel felé hanem hiszen hogy hogyan igen ill ill. illetve ilyen ilyenkor ismét ison itt jobban jó jól kell kellett keressünk keresztül ki kívül között közül legalább legyen lehet lehetett lenne lenni lesz lett maga magát majd majd meg mellett mely melyek mert mi mikor milyen minden mindenki mindent mindig mint mintha mit mivel miért most már más másik még míg nagy nagyobb nagyon ne nekem neki nem nincs néha néhány nélkül olyan ott pedig persze rá s saját sem semmi sok sokat sokkal szemben szerint szinte számára talán tehát teljes tovább továbbá több ugyanis utolsó után utána vagy vagyis vagyok valaki valami valamint való van vannak vele vissza viszont volna volt voltak voltam voltunk által általában át én éppen és így õ õk õket össze úgy új újabb újra".split(" ")),e.Pipeline.registerFunction(e.hu.stopWordFilter,"stopWordFilter-hu")}}); \ No newline at end of file diff --git a/v0.13.6/assets/javascripts/lunr/min/lunr.it.min.js b/v0.13.6/assets/javascripts/lunr/min/lunr.it.min.js new file mode 100644 index 0000000000..344b6a3c0c --- /dev/null +++ b/v0.13.6/assets/javascripts/lunr/min/lunr.it.min.js @@ -0,0 +1,18 @@ +/*! + * Lunr languages, `Italian` language + * https://github.com/MihaiValentin/lunr-languages + * + * Copyright 2014, Mihai Valentin + * http://www.mozilla.org/MPL/ + */ +/*! + * based on + * Snowball JavaScript Library v0.3 + * http://code.google.com/p/urim/ + * http://snowball.tartarus.org/ + * + * Copyright 2010, Oleg Mazko + * http://www.mozilla.org/MPL/ + */ + +!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.it=function(){this.pipeline.reset(),this.pipeline.add(e.it.trimmer,e.it.stopWordFilter,e.it.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.it.stemmer))},e.it.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",e.it.trimmer=e.trimmerSupport.generateTrimmer(e.it.wordCharacters),e.Pipeline.registerFunction(e.it.trimmer,"trimmer-it"),e.it.stemmer=function(){var r=e.stemmerSupport.Among,n=e.stemmerSupport.SnowballProgram,i=new function(){function e(e,r,n){return!(!x.eq_s(1,e)||(x.ket=x.cursor,!x.in_grouping(L,97,249)))&&(x.slice_from(r),x.cursor=n,!0)}function i(){for(var r,n,i,o,t=x.cursor;;){if(x.bra=x.cursor,r=x.find_among(h,7))switch(x.ket=x.cursor,r){case 1:x.slice_from("à");continue;case 2:x.slice_from("è");continue;case 3:x.slice_from("ì");continue;case 4:x.slice_from("ò");continue;case 5:x.slice_from("ù");continue;case 6:x.slice_from("qU");continue;case 7:if(x.cursor>=x.limit)break;x.cursor++;continue}break}for(x.cursor=t;;)for(n=x.cursor;;){if(i=x.cursor,x.in_grouping(L,97,249)){if(x.bra=x.cursor,o=x.cursor,e("u","U",i))break;if(x.cursor=o,e("i","I",i))break}if(x.cursor=i,x.cursor>=x.limit)return void(x.cursor=n);x.cursor++}}function o(e){if(x.cursor=e,!x.in_grouping(L,97,249))return!1;for(;!x.out_grouping(L,97,249);){if(x.cursor>=x.limit)return!1;x.cursor++}return!0}function t(){if(x.in_grouping(L,97,249)){var e=x.cursor;if(x.out_grouping(L,97,249)){for(;!x.in_grouping(L,97,249);){if(x.cursor>=x.limit)return o(e);x.cursor++}return!0}return o(e)}return!1}function s(){var e,r=x.cursor;if(!t()){if(x.cursor=r,!x.out_grouping(L,97,249))return;if(e=x.cursor,x.out_grouping(L,97,249)){for(;!x.in_grouping(L,97,249);){if(x.cursor>=x.limit)return x.cursor=e,void(x.in_grouping(L,97,249)&&x.cursor=x.limit)return;x.cursor++}k=x.cursor}function a(){for(;!x.in_grouping(L,97,249);){if(x.cursor>=x.limit)return!1;x.cursor++}for(;!x.out_grouping(L,97,249);){if(x.cursor>=x.limit)return!1;x.cursor++}return!0}function u(){var e=x.cursor;k=x.limit,p=k,g=k,s(),x.cursor=e,a()&&(p=x.cursor,a()&&(g=x.cursor))}function c(){for(var e;;){if(x.bra=x.cursor,!(e=x.find_among(q,3)))break;switch(x.ket=x.cursor,e){case 1:x.slice_from("i");break;case 2:x.slice_from("u");break;case 3:if(x.cursor>=x.limit)return;x.cursor++}}}function w(){return k<=x.cursor}function l(){return p<=x.cursor}function m(){return g<=x.cursor}function f(){var e;if(x.ket=x.cursor,x.find_among_b(C,37)&&(x.bra=x.cursor,(e=x.find_among_b(z,5))&&w()))switch(e){case 1:x.slice_del();break;case 2:x.slice_from("e")}}function v(){var e;if(x.ket=x.cursor,!(e=x.find_among_b(S,51)))return!1;switch(x.bra=x.cursor,e){case 1:if(!m())return!1;x.slice_del();break;case 2:if(!m())return!1;x.slice_del(),x.ket=x.cursor,x.eq_s_b(2,"ic")&&(x.bra=x.cursor,m()&&x.slice_del());break;case 3:if(!m())return!1;x.slice_from("log");break;case 4:if(!m())return!1;x.slice_from("u");break;case 5:if(!m())return!1;x.slice_from("ente");break;case 6:if(!w())return!1;x.slice_del();break;case 7:if(!l())return!1;x.slice_del(),x.ket=x.cursor,e=x.find_among_b(P,4),e&&(x.bra=x.cursor,m()&&(x.slice_del(),1==e&&(x.ket=x.cursor,x.eq_s_b(2,"at")&&(x.bra=x.cursor,m()&&x.slice_del()))));break;case 8:if(!m())return!1;x.slice_del(),x.ket=x.cursor,e=x.find_among_b(F,3),e&&(x.bra=x.cursor,1==e&&m()&&x.slice_del());break;case 9:if(!m())return!1;x.slice_del(),x.ket=x.cursor,x.eq_s_b(2,"at")&&(x.bra=x.cursor,m()&&(x.slice_del(),x.ket=x.cursor,x.eq_s_b(2,"ic")&&(x.bra=x.cursor,m()&&x.slice_del())))}return!0}function b(){var e,r;x.cursor>=k&&(r=x.limit_backward,x.limit_backward=k,x.ket=x.cursor,e=x.find_among_b(W,87),e&&(x.bra=x.cursor,1==e&&x.slice_del()),x.limit_backward=r)}function d(){var e=x.limit-x.cursor;if(x.ket=x.cursor,x.in_grouping_b(y,97,242)&&(x.bra=x.cursor,w()&&(x.slice_del(),x.ket=x.cursor,x.eq_s_b(1,"i")&&(x.bra=x.cursor,w()))))return void x.slice_del();x.cursor=x.limit-e}function _(){d(),x.ket=x.cursor,x.eq_s_b(1,"h")&&(x.bra=x.cursor,x.in_grouping_b(U,99,103)&&w()&&x.slice_del())}var g,p,k,h=[new r("",-1,7),new r("qu",0,6),new r("á",0,1),new r("é",0,2),new r("í",0,3),new r("ó",0,4),new r("ú",0,5)],q=[new r("",-1,3),new r("I",0,1),new r("U",0,2)],C=[new r("la",-1,-1),new r("cela",0,-1),new r("gliela",0,-1),new r("mela",0,-1),new r("tela",0,-1),new r("vela",0,-1),new r("le",-1,-1),new r("cele",6,-1),new r("gliele",6,-1),new r("mele",6,-1),new r("tele",6,-1),new r("vele",6,-1),new r("ne",-1,-1),new r("cene",12,-1),new r("gliene",12,-1),new r("mene",12,-1),new r("sene",12,-1),new r("tene",12,-1),new r("vene",12,-1),new r("ci",-1,-1),new r("li",-1,-1),new r("celi",20,-1),new r("glieli",20,-1),new r("meli",20,-1),new r("teli",20,-1),new r("veli",20,-1),new r("gli",20,-1),new r("mi",-1,-1),new r("si",-1,-1),new r("ti",-1,-1),new r("vi",-1,-1),new r("lo",-1,-1),new r("celo",31,-1),new r("glielo",31,-1),new r("melo",31,-1),new r("telo",31,-1),new r("velo",31,-1)],z=[new r("ando",-1,1),new r("endo",-1,1),new r("ar",-1,2),new r("er",-1,2),new r("ir",-1,2)],P=[new r("ic",-1,-1),new r("abil",-1,-1),new r("os",-1,-1),new r("iv",-1,1)],F=[new r("ic",-1,1),new r("abil",-1,1),new r("iv",-1,1)],S=[new r("ica",-1,1),new r("logia",-1,3),new r("osa",-1,1),new r("ista",-1,1),new r("iva",-1,9),new r("anza",-1,1),new r("enza",-1,5),new r("ice",-1,1),new r("atrice",7,1),new r("iche",-1,1),new r("logie",-1,3),new r("abile",-1,1),new r("ibile",-1,1),new r("usione",-1,4),new r("azione",-1,2),new r("uzione",-1,4),new r("atore",-1,2),new r("ose",-1,1),new r("ante",-1,1),new r("mente",-1,1),new r("amente",19,7),new r("iste",-1,1),new r("ive",-1,9),new r("anze",-1,1),new r("enze",-1,5),new r("ici",-1,1),new r("atrici",25,1),new r("ichi",-1,1),new r("abili",-1,1),new r("ibili",-1,1),new r("ismi",-1,1),new r("usioni",-1,4),new r("azioni",-1,2),new r("uzioni",-1,4),new r("atori",-1,2),new r("osi",-1,1),new r("anti",-1,1),new r("amenti",-1,6),new r("imenti",-1,6),new r("isti",-1,1),new r("ivi",-1,9),new r("ico",-1,1),new r("ismo",-1,1),new r("oso",-1,1),new r("amento",-1,6),new r("imento",-1,6),new r("ivo",-1,9),new r("ità",-1,8),new r("istà",-1,1),new r("istè",-1,1),new r("istì",-1,1)],W=[new r("isca",-1,1),new r("enda",-1,1),new r("ata",-1,1),new r("ita",-1,1),new r("uta",-1,1),new r("ava",-1,1),new r("eva",-1,1),new r("iva",-1,1),new r("erebbe",-1,1),new r("irebbe",-1,1),new r("isce",-1,1),new r("ende",-1,1),new r("are",-1,1),new r("ere",-1,1),new r("ire",-1,1),new r("asse",-1,1),new r("ate",-1,1),new r("avate",16,1),new r("evate",16,1),new r("ivate",16,1),new r("ete",-1,1),new r("erete",20,1),new r("irete",20,1),new r("ite",-1,1),new r("ereste",-1,1),new r("ireste",-1,1),new r("ute",-1,1),new r("erai",-1,1),new r("irai",-1,1),new r("isci",-1,1),new r("endi",-1,1),new r("erei",-1,1),new r("irei",-1,1),new r("assi",-1,1),new r("ati",-1,1),new r("iti",-1,1),new r("eresti",-1,1),new r("iresti",-1,1),new r("uti",-1,1),new r("avi",-1,1),new r("evi",-1,1),new r("ivi",-1,1),new r("isco",-1,1),new r("ando",-1,1),new r("endo",-1,1),new r("Yamo",-1,1),new r("iamo",-1,1),new r("avamo",-1,1),new r("evamo",-1,1),new r("ivamo",-1,1),new r("eremo",-1,1),new r("iremo",-1,1),new r("assimo",-1,1),new r("ammo",-1,1),new r("emmo",-1,1),new r("eremmo",54,1),new r("iremmo",54,1),new r("immo",-1,1),new r("ano",-1,1),new r("iscano",58,1),new r("avano",58,1),new r("evano",58,1),new r("ivano",58,1),new r("eranno",-1,1),new r("iranno",-1,1),new r("ono",-1,1),new r("iscono",65,1),new r("arono",65,1),new r("erono",65,1),new r("irono",65,1),new r("erebbero",-1,1),new r("irebbero",-1,1),new r("assero",-1,1),new r("essero",-1,1),new r("issero",-1,1),new r("ato",-1,1),new r("ito",-1,1),new r("uto",-1,1),new r("avo",-1,1),new r("evo",-1,1),new r("ivo",-1,1),new r("ar",-1,1),new r("ir",-1,1),new r("erà",-1,1),new r("irà",-1,1),new r("erò",-1,1),new r("irò",-1,1)],L=[17,65,16,0,0,0,0,0,0,0,0,0,0,0,0,128,128,8,2,1],y=[17,65,0,0,0,0,0,0,0,0,0,0,0,0,0,128,128,8,2],U=[17],x=new n;this.setCurrent=function(e){x.setCurrent(e)},this.getCurrent=function(){return x.getCurrent()},this.stem=function(){var e=x.cursor;return i(),x.cursor=e,u(),x.limit_backward=e,x.cursor=x.limit,f(),x.cursor=x.limit,v()||(x.cursor=x.limit,b()),x.cursor=x.limit,_(),x.cursor=x.limit_backward,c(),!0}};return function(e){return"function"==typeof e.update?e.update(function(e){return i.setCurrent(e),i.stem(),i.getCurrent()}):(i.setCurrent(e),i.stem(),i.getCurrent())}}(),e.Pipeline.registerFunction(e.it.stemmer,"stemmer-it"),e.it.stopWordFilter=e.generateStopWordFilter("a abbia abbiamo abbiano abbiate ad agl agli ai al all alla alle allo anche avemmo avendo avesse avessero avessi avessimo aveste avesti avete aveva avevamo avevano avevate avevi avevo avrai avranno avrebbe avrebbero avrei avremmo avremo avreste avresti avrete avrà avrò avuta avute avuti avuto c che chi ci coi col come con contro cui da dagl dagli dai dal dall dalla dalle dallo degl degli dei del dell della delle dello di dov dove e ebbe ebbero ebbi ed era erano eravamo eravate eri ero essendo faccia facciamo facciano facciate faccio facemmo facendo facesse facessero facessi facessimo faceste facesti faceva facevamo facevano facevate facevi facevo fai fanno farai faranno farebbe farebbero farei faremmo faremo fareste faresti farete farà farò fece fecero feci fosse fossero fossi fossimo foste fosti fu fui fummo furono gli ha hai hanno ho i il in io l la le lei li lo loro lui ma mi mia mie miei mio ne negl negli nei nel nell nella nelle nello noi non nostra nostre nostri nostro o per perché più quale quanta quante quanti quanto quella quelle quelli quello questa queste questi questo sarai saranno sarebbe sarebbero sarei saremmo saremo sareste saresti sarete sarà sarò se sei si sia siamo siano siate siete sono sta stai stando stanno starai staranno starebbe starebbero starei staremmo staremo stareste staresti starete starà starò stava stavamo stavano stavate stavi stavo stemmo stesse stessero stessi stessimo steste stesti stette stettero stetti stia stiamo stiano stiate sto su sua sue sugl sugli sui sul sull sulla sulle sullo suo suoi ti tra tu tua tue tuo tuoi tutti tutto un una uno vi voi vostra vostre vostri vostro è".split(" ")),e.Pipeline.registerFunction(e.it.stopWordFilter,"stopWordFilter-it")}}); \ No newline at end of file diff --git a/v0.13.6/assets/javascripts/lunr/min/lunr.ja.min.js b/v0.13.6/assets/javascripts/lunr/min/lunr.ja.min.js new file mode 100644 index 0000000000..5f254ebe91 --- /dev/null +++ b/v0.13.6/assets/javascripts/lunr/min/lunr.ja.min.js @@ -0,0 +1 @@ +!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");var r="2"==e.version[0];e.ja=function(){this.pipeline.reset(),this.pipeline.add(e.ja.trimmer,e.ja.stopWordFilter,e.ja.stemmer),r?this.tokenizer=e.ja.tokenizer:(e.tokenizer&&(e.tokenizer=e.ja.tokenizer),this.tokenizerFn&&(this.tokenizerFn=e.ja.tokenizer))};var t=new e.TinySegmenter;e.ja.tokenizer=function(i){var n,o,s,p,a,u,m,l,c,f;if(!arguments.length||null==i||void 0==i)return[];if(Array.isArray(i))return i.map(function(t){return r?new e.Token(t.toLowerCase()):t.toLowerCase()});for(o=i.toString().toLowerCase().replace(/^\s+/,""),n=o.length-1;n>=0;n--)if(/\S/.test(o.charAt(n))){o=o.substring(0,n+1);break}for(a=[],s=o.length,c=0,l=0;c<=s;c++)if(u=o.charAt(c),m=c-l,u.match(/\s/)||c==s){if(m>0)for(p=t.segment(o.slice(l,c)).filter(function(e){return!!e}),f=l,n=0;n=C.limit)break;C.cursor++;continue}break}for(C.cursor=o,C.bra=o,C.eq_s(1,"y")?(C.ket=C.cursor,C.slice_from("Y")):C.cursor=o;;)if(e=C.cursor,C.in_grouping(q,97,232)){if(i=C.cursor,C.bra=i,C.eq_s(1,"i"))C.ket=C.cursor,C.in_grouping(q,97,232)&&(C.slice_from("I"),C.cursor=e);else if(C.cursor=i,C.eq_s(1,"y"))C.ket=C.cursor,C.slice_from("Y"),C.cursor=e;else if(n(e))break}else if(n(e))break}function n(r){return C.cursor=r,r>=C.limit||(C.cursor++,!1)}function o(){_=C.limit,d=_,t()||(_=C.cursor,_<3&&(_=3),t()||(d=C.cursor))}function t(){for(;!C.in_grouping(q,97,232);){if(C.cursor>=C.limit)return!0;C.cursor++}for(;!C.out_grouping(q,97,232);){if(C.cursor>=C.limit)return!0;C.cursor++}return!1}function s(){for(var r;;)if(C.bra=C.cursor,r=C.find_among(p,3))switch(C.ket=C.cursor,r){case 1:C.slice_from("y");break;case 2:C.slice_from("i");break;case 3:if(C.cursor>=C.limit)return;C.cursor++}}function u(){return _<=C.cursor}function c(){return d<=C.cursor}function a(){var r=C.limit-C.cursor;C.find_among_b(g,3)&&(C.cursor=C.limit-r,C.ket=C.cursor,C.cursor>C.limit_backward&&(C.cursor--,C.bra=C.cursor,C.slice_del()))}function l(){var r;w=!1,C.ket=C.cursor,C.eq_s_b(1,"e")&&(C.bra=C.cursor,u()&&(r=C.limit-C.cursor,C.out_grouping_b(q,97,232)&&(C.cursor=C.limit-r,C.slice_del(),w=!0,a())))}function m(){var r;u()&&(r=C.limit-C.cursor,C.out_grouping_b(q,97,232)&&(C.cursor=C.limit-r,C.eq_s_b(3,"gem")||(C.cursor=C.limit-r,C.slice_del(),a())))}function f(){var r,e,i,n,o,t,s=C.limit-C.cursor;if(C.ket=C.cursor,r=C.find_among_b(h,5))switch(C.bra=C.cursor,r){case 1:u()&&C.slice_from("heid");break;case 2:m();break;case 3:u()&&C.out_grouping_b(j,97,232)&&C.slice_del()}if(C.cursor=C.limit-s,l(),C.cursor=C.limit-s,C.ket=C.cursor,C.eq_s_b(4,"heid")&&(C.bra=C.cursor,c()&&(e=C.limit-C.cursor,C.eq_s_b(1,"c")||(C.cursor=C.limit-e,C.slice_del(),C.ket=C.cursor,C.eq_s_b(2,"en")&&(C.bra=C.cursor,m())))),C.cursor=C.limit-s,C.ket=C.cursor,r=C.find_among_b(k,6))switch(C.bra=C.cursor,r){case 1:if(c()){if(C.slice_del(),i=C.limit-C.cursor,C.ket=C.cursor,C.eq_s_b(2,"ig")&&(C.bra=C.cursor,c()&&(n=C.limit-C.cursor,!C.eq_s_b(1,"e")))){C.cursor=C.limit-n,C.slice_del();break}C.cursor=C.limit-i,a()}break;case 2:c()&&(o=C.limit-C.cursor,C.eq_s_b(1,"e")||(C.cursor=C.limit-o,C.slice_del()));break;case 3:c()&&(C.slice_del(),l());break;case 4:c()&&C.slice_del();break;case 5:c()&&w&&C.slice_del()}C.cursor=C.limit-s,C.out_grouping_b(z,73,232)&&(t=C.limit-C.cursor,C.find_among_b(v,4)&&C.out_grouping_b(q,97,232)&&(C.cursor=C.limit-t,C.ket=C.cursor,C.cursor>C.limit_backward&&(C.cursor--,C.bra=C.cursor,C.slice_del())))}var d,_,w,b=[new e("",-1,6),new e("á",0,1),new e("ä",0,1),new e("é",0,2),new e("ë",0,2),new e("í",0,3),new e("ï",0,3),new e("ó",0,4),new e("ö",0,4),new e("ú",0,5),new e("ü",0,5)],p=[new e("",-1,3),new e("I",0,2),new e("Y",0,1)],g=[new e("dd",-1,-1),new e("kk",-1,-1),new e("tt",-1,-1)],h=[new e("ene",-1,2),new e("se",-1,3),new e("en",-1,2),new e("heden",2,1),new e("s",-1,3)],k=[new e("end",-1,1),new e("ig",-1,2),new e("ing",-1,1),new e("lijk",-1,3),new e("baar",-1,4),new e("bar",-1,5)],v=[new e("aa",-1,-1),new e("ee",-1,-1),new e("oo",-1,-1),new e("uu",-1,-1)],q=[17,65,16,1,0,0,0,0,0,0,0,0,0,0,0,0,128],z=[1,0,0,17,65,16,1,0,0,0,0,0,0,0,0,0,0,0,0,128],j=[17,67,16,1,0,0,0,0,0,0,0,0,0,0,0,0,128],C=new i;this.setCurrent=function(r){C.setCurrent(r)},this.getCurrent=function(){return C.getCurrent()},this.stem=function(){var e=C.cursor;return r(),C.cursor=e,o(),C.limit_backward=e,C.cursor=C.limit,f(),C.cursor=C.limit_backward,s(),!0}};return function(r){return"function"==typeof r.update?r.update(function(r){return n.setCurrent(r),n.stem(),n.getCurrent()}):(n.setCurrent(r),n.stem(),n.getCurrent())}}(),r.Pipeline.registerFunction(r.nl.stemmer,"stemmer-nl"),r.nl.stopWordFilter=r.generateStopWordFilter(" aan al alles als altijd andere ben bij daar dan dat de der deze die dit doch doen door dus een eens en er ge geen geweest haar had heb hebben heeft hem het hier hij hoe hun iemand iets ik in is ja je kan kon kunnen maar me meer men met mij mijn moet na naar niet niets nog nu of om omdat onder ons ook op over reeds te tegen toch toen tot u uit uw van veel voor want waren was wat werd wezen wie wil worden wordt zal ze zelf zich zij zijn zo zonder zou".split(" ")),r.Pipeline.registerFunction(r.nl.stopWordFilter,"stopWordFilter-nl")}}); \ No newline at end of file diff --git a/v0.13.6/assets/javascripts/lunr/min/lunr.no.min.js b/v0.13.6/assets/javascripts/lunr/min/lunr.no.min.js new file mode 100644 index 0000000000..92bc7e4e89 --- /dev/null +++ b/v0.13.6/assets/javascripts/lunr/min/lunr.no.min.js @@ -0,0 +1,18 @@ +/*! + * Lunr languages, `Norwegian` language + * https://github.com/MihaiValentin/lunr-languages + * + * Copyright 2014, Mihai Valentin + * http://www.mozilla.org/MPL/ + */ +/*! + * based on + * Snowball JavaScript Library v0.3 + * http://code.google.com/p/urim/ + * http://snowball.tartarus.org/ + * + * Copyright 2010, Oleg Mazko + * http://www.mozilla.org/MPL/ + */ + +!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.no=function(){this.pipeline.reset(),this.pipeline.add(e.no.trimmer,e.no.stopWordFilter,e.no.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.no.stemmer))},e.no.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",e.no.trimmer=e.trimmerSupport.generateTrimmer(e.no.wordCharacters),e.Pipeline.registerFunction(e.no.trimmer,"trimmer-no"),e.no.stemmer=function(){var r=e.stemmerSupport.Among,n=e.stemmerSupport.SnowballProgram,i=new function(){function e(){var e,r=w.cursor+3;if(a=w.limit,0<=r||r<=w.limit){for(s=r;;){if(e=w.cursor,w.in_grouping(d,97,248)){w.cursor=e;break}if(e>=w.limit)return;w.cursor=e+1}for(;!w.out_grouping(d,97,248);){if(w.cursor>=w.limit)return;w.cursor++}a=w.cursor,a=a&&(r=w.limit_backward,w.limit_backward=a,w.ket=w.cursor,e=w.find_among_b(m,29),w.limit_backward=r,e))switch(w.bra=w.cursor,e){case 1:w.slice_del();break;case 2:n=w.limit-w.cursor,w.in_grouping_b(c,98,122)?w.slice_del():(w.cursor=w.limit-n,w.eq_s_b(1,"k")&&w.out_grouping_b(d,97,248)&&w.slice_del());break;case 3:w.slice_from("er")}}function t(){var e,r=w.limit-w.cursor;w.cursor>=a&&(e=w.limit_backward,w.limit_backward=a,w.ket=w.cursor,w.find_among_b(u,2)?(w.bra=w.cursor,w.limit_backward=e,w.cursor=w.limit-r,w.cursor>w.limit_backward&&(w.cursor--,w.bra=w.cursor,w.slice_del())):w.limit_backward=e)}function o(){var e,r;w.cursor>=a&&(r=w.limit_backward,w.limit_backward=a,w.ket=w.cursor,e=w.find_among_b(l,11),e?(w.bra=w.cursor,w.limit_backward=r,1==e&&w.slice_del()):w.limit_backward=r)}var s,a,m=[new r("a",-1,1),new r("e",-1,1),new r("ede",1,1),new r("ande",1,1),new r("ende",1,1),new r("ane",1,1),new r("ene",1,1),new r("hetene",6,1),new r("erte",1,3),new r("en",-1,1),new r("heten",9,1),new r("ar",-1,1),new r("er",-1,1),new r("heter",12,1),new r("s",-1,2),new r("as",14,1),new r("es",14,1),new r("edes",16,1),new r("endes",16,1),new r("enes",16,1),new r("hetenes",19,1),new r("ens",14,1),new r("hetens",21,1),new r("ers",14,1),new r("ets",14,1),new r("et",-1,1),new r("het",25,1),new r("ert",-1,3),new r("ast",-1,1)],u=[new r("dt",-1,-1),new r("vt",-1,-1)],l=[new r("leg",-1,1),new r("eleg",0,1),new r("ig",-1,1),new r("eig",2,1),new r("lig",2,1),new r("elig",4,1),new r("els",-1,1),new r("lov",-1,1),new r("elov",7,1),new r("slov",7,1),new r("hetslov",9,1)],d=[17,65,16,1,0,0,0,0,0,0,0,0,0,0,0,0,48,0,128],c=[119,125,149,1],w=new n;this.setCurrent=function(e){w.setCurrent(e)},this.getCurrent=function(){return w.getCurrent()},this.stem=function(){var r=w.cursor;return e(),w.limit_backward=r,w.cursor=w.limit,i(),w.cursor=w.limit,t(),w.cursor=w.limit,o(),!0}};return function(e){return"function"==typeof e.update?e.update(function(e){return i.setCurrent(e),i.stem(),i.getCurrent()}):(i.setCurrent(e),i.stem(),i.getCurrent())}}(),e.Pipeline.registerFunction(e.no.stemmer,"stemmer-no"),e.no.stopWordFilter=e.generateStopWordFilter("alle at av bare begge ble blei bli blir blitt både båe da de deg dei deim deira deires dem den denne der dere deres det dette di din disse ditt du dykk dykkar då eg ein eit eitt eller elles en enn er et ett etter for fordi fra før ha hadde han hans har hennar henne hennes her hjå ho hoe honom hoss hossen hun hva hvem hver hvilke hvilken hvis hvor hvordan hvorfor i ikke ikkje ikkje ingen ingi inkje inn inni ja jeg kan kom korleis korso kun kunne kva kvar kvarhelst kven kvi kvifor man mange me med medan meg meget mellom men mi min mine mitt mot mykje ned no noe noen noka noko nokon nokor nokre nå når og også om opp oss over på samme seg selv si si sia sidan siden sin sine sitt sjøl skal skulle slik so som som somme somt så sånn til um upp ut uten var vart varte ved vere verte vi vil ville vore vors vort vår være være vært å".split(" ")),e.Pipeline.registerFunction(e.no.stopWordFilter,"stopWordFilter-no")}}); \ No newline at end of file diff --git a/v0.13.6/assets/javascripts/lunr/min/lunr.pt.min.js b/v0.13.6/assets/javascripts/lunr/min/lunr.pt.min.js new file mode 100644 index 0000000000..6c16996d65 --- /dev/null +++ b/v0.13.6/assets/javascripts/lunr/min/lunr.pt.min.js @@ -0,0 +1,18 @@ +/*! + * Lunr languages, `Portuguese` language + * https://github.com/MihaiValentin/lunr-languages + * + * Copyright 2014, Mihai Valentin + * http://www.mozilla.org/MPL/ + */ +/*! + * based on + * Snowball JavaScript Library v0.3 + * http://code.google.com/p/urim/ + * http://snowball.tartarus.org/ + * + * Copyright 2010, Oleg Mazko + * http://www.mozilla.org/MPL/ + */ + +!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.pt=function(){this.pipeline.reset(),this.pipeline.add(e.pt.trimmer,e.pt.stopWordFilter,e.pt.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.pt.stemmer))},e.pt.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",e.pt.trimmer=e.trimmerSupport.generateTrimmer(e.pt.wordCharacters),e.Pipeline.registerFunction(e.pt.trimmer,"trimmer-pt"),e.pt.stemmer=function(){var r=e.stemmerSupport.Among,s=e.stemmerSupport.SnowballProgram,n=new function(){function e(){for(var e;;){if(z.bra=z.cursor,e=z.find_among(k,3))switch(z.ket=z.cursor,e){case 1:z.slice_from("a~");continue;case 2:z.slice_from("o~");continue;case 3:if(z.cursor>=z.limit)break;z.cursor++;continue}break}}function n(){if(z.out_grouping(y,97,250)){for(;!z.in_grouping(y,97,250);){if(z.cursor>=z.limit)return!0;z.cursor++}return!1}return!0}function i(){if(z.in_grouping(y,97,250))for(;!z.out_grouping(y,97,250);){if(z.cursor>=z.limit)return!1;z.cursor++}return g=z.cursor,!0}function o(){var e,r,s=z.cursor;if(z.in_grouping(y,97,250))if(e=z.cursor,n()){if(z.cursor=e,i())return}else g=z.cursor;if(z.cursor=s,z.out_grouping(y,97,250)){if(r=z.cursor,n()){if(z.cursor=r,!z.in_grouping(y,97,250)||z.cursor>=z.limit)return;z.cursor++}g=z.cursor}}function t(){for(;!z.in_grouping(y,97,250);){if(z.cursor>=z.limit)return!1;z.cursor++}for(;!z.out_grouping(y,97,250);){if(z.cursor>=z.limit)return!1;z.cursor++}return!0}function a(){var e=z.cursor;g=z.limit,b=g,h=g,o(),z.cursor=e,t()&&(b=z.cursor,t()&&(h=z.cursor))}function u(){for(var e;;){if(z.bra=z.cursor,e=z.find_among(q,3))switch(z.ket=z.cursor,e){case 1:z.slice_from("ã");continue;case 2:z.slice_from("õ");continue;case 3:if(z.cursor>=z.limit)break;z.cursor++;continue}break}}function w(){return g<=z.cursor}function m(){return b<=z.cursor}function c(){return h<=z.cursor}function l(){var e;if(z.ket=z.cursor,!(e=z.find_among_b(F,45)))return!1;switch(z.bra=z.cursor,e){case 1:if(!c())return!1;z.slice_del();break;case 2:if(!c())return!1;z.slice_from("log");break;case 3:if(!c())return!1;z.slice_from("u");break;case 4:if(!c())return!1;z.slice_from("ente");break;case 5:if(!m())return!1;z.slice_del(),z.ket=z.cursor,e=z.find_among_b(j,4),e&&(z.bra=z.cursor,c()&&(z.slice_del(),1==e&&(z.ket=z.cursor,z.eq_s_b(2,"at")&&(z.bra=z.cursor,c()&&z.slice_del()))));break;case 6:if(!c())return!1;z.slice_del(),z.ket=z.cursor,e=z.find_among_b(C,3),e&&(z.bra=z.cursor,1==e&&c()&&z.slice_del());break;case 7:if(!c())return!1;z.slice_del(),z.ket=z.cursor,e=z.find_among_b(P,3),e&&(z.bra=z.cursor,1==e&&c()&&z.slice_del());break;case 8:if(!c())return!1;z.slice_del(),z.ket=z.cursor,z.eq_s_b(2,"at")&&(z.bra=z.cursor,c()&&z.slice_del());break;case 9:if(!w()||!z.eq_s_b(1,"e"))return!1;z.slice_from("ir")}return!0}function f(){var e,r;if(z.cursor>=g){if(r=z.limit_backward,z.limit_backward=g,z.ket=z.cursor,e=z.find_among_b(S,120))return z.bra=z.cursor,1==e&&z.slice_del(),z.limit_backward=r,!0;z.limit_backward=r}return!1}function d(){var e;z.ket=z.cursor,(e=z.find_among_b(W,7))&&(z.bra=z.cursor,1==e&&w()&&z.slice_del())}function v(e,r){if(z.eq_s_b(1,e)){z.bra=z.cursor;var s=z.limit-z.cursor;if(z.eq_s_b(1,r))return z.cursor=z.limit-s,w()&&z.slice_del(),!1}return!0}function p(){var e;if(z.ket=z.cursor,e=z.find_among_b(L,4))switch(z.bra=z.cursor,e){case 1:w()&&(z.slice_del(),z.ket=z.cursor,z.limit-z.cursor,v("u","g")&&v("i","c"));break;case 2:z.slice_from("c")}}function _(){if(!l()&&(z.cursor=z.limit,!f()))return z.cursor=z.limit,void d();z.cursor=z.limit,z.ket=z.cursor,z.eq_s_b(1,"i")&&(z.bra=z.cursor,z.eq_s_b(1,"c")&&(z.cursor=z.limit,w()&&z.slice_del()))}var h,b,g,k=[new r("",-1,3),new r("ã",0,1),new r("õ",0,2)],q=[new r("",-1,3),new r("a~",0,1),new r("o~",0,2)],j=[new r("ic",-1,-1),new r("ad",-1,-1),new r("os",-1,-1),new r("iv",-1,1)],C=[new r("ante",-1,1),new r("avel",-1,1),new r("ível",-1,1)],P=[new r("ic",-1,1),new r("abil",-1,1),new r("iv",-1,1)],F=[new r("ica",-1,1),new r("ância",-1,1),new r("ência",-1,4),new r("ira",-1,9),new r("adora",-1,1),new r("osa",-1,1),new r("ista",-1,1),new r("iva",-1,8),new r("eza",-1,1),new r("logía",-1,2),new r("idade",-1,7),new r("ante",-1,1),new r("mente",-1,6),new r("amente",12,5),new r("ável",-1,1),new r("ível",-1,1),new r("ución",-1,3),new r("ico",-1,1),new r("ismo",-1,1),new r("oso",-1,1),new r("amento",-1,1),new r("imento",-1,1),new r("ivo",-1,8),new r("aça~o",-1,1),new r("ador",-1,1),new r("icas",-1,1),new r("ências",-1,4),new r("iras",-1,9),new r("adoras",-1,1),new r("osas",-1,1),new r("istas",-1,1),new r("ivas",-1,8),new r("ezas",-1,1),new r("logías",-1,2),new r("idades",-1,7),new r("uciones",-1,3),new r("adores",-1,1),new r("antes",-1,1),new r("aço~es",-1,1),new r("icos",-1,1),new r("ismos",-1,1),new r("osos",-1,1),new r("amentos",-1,1),new r("imentos",-1,1),new r("ivos",-1,8)],S=[new r("ada",-1,1),new r("ida",-1,1),new r("ia",-1,1),new r("aria",2,1),new r("eria",2,1),new r("iria",2,1),new r("ara",-1,1),new r("era",-1,1),new r("ira",-1,1),new r("ava",-1,1),new r("asse",-1,1),new r("esse",-1,1),new r("isse",-1,1),new r("aste",-1,1),new r("este",-1,1),new r("iste",-1,1),new r("ei",-1,1),new r("arei",16,1),new r("erei",16,1),new r("irei",16,1),new r("am",-1,1),new r("iam",20,1),new r("ariam",21,1),new r("eriam",21,1),new r("iriam",21,1),new r("aram",20,1),new r("eram",20,1),new r("iram",20,1),new r("avam",20,1),new r("em",-1,1),new r("arem",29,1),new r("erem",29,1),new r("irem",29,1),new r("assem",29,1),new r("essem",29,1),new r("issem",29,1),new r("ado",-1,1),new r("ido",-1,1),new r("ando",-1,1),new r("endo",-1,1),new r("indo",-1,1),new r("ara~o",-1,1),new r("era~o",-1,1),new r("ira~o",-1,1),new r("ar",-1,1),new r("er",-1,1),new r("ir",-1,1),new r("as",-1,1),new r("adas",47,1),new r("idas",47,1),new r("ias",47,1),new r("arias",50,1),new r("erias",50,1),new r("irias",50,1),new r("aras",47,1),new r("eras",47,1),new r("iras",47,1),new r("avas",47,1),new r("es",-1,1),new r("ardes",58,1),new r("erdes",58,1),new r("irdes",58,1),new r("ares",58,1),new r("eres",58,1),new r("ires",58,1),new r("asses",58,1),new r("esses",58,1),new r("isses",58,1),new r("astes",58,1),new r("estes",58,1),new r("istes",58,1),new r("is",-1,1),new r("ais",71,1),new r("eis",71,1),new r("areis",73,1),new r("ereis",73,1),new r("ireis",73,1),new r("áreis",73,1),new r("éreis",73,1),new r("íreis",73,1),new r("ásseis",73,1),new r("ésseis",73,1),new r("ísseis",73,1),new r("áveis",73,1),new r("íeis",73,1),new r("aríeis",84,1),new r("eríeis",84,1),new r("iríeis",84,1),new r("ados",-1,1),new r("idos",-1,1),new r("amos",-1,1),new r("áramos",90,1),new r("éramos",90,1),new r("íramos",90,1),new r("ávamos",90,1),new r("íamos",90,1),new r("aríamos",95,1),new r("eríamos",95,1),new r("iríamos",95,1),new r("emos",-1,1),new r("aremos",99,1),new r("eremos",99,1),new r("iremos",99,1),new r("ássemos",99,1),new r("êssemos",99,1),new r("íssemos",99,1),new r("imos",-1,1),new r("armos",-1,1),new r("ermos",-1,1),new r("irmos",-1,1),new r("ámos",-1,1),new r("arás",-1,1),new r("erás",-1,1),new r("irás",-1,1),new r("eu",-1,1),new r("iu",-1,1),new r("ou",-1,1),new r("ará",-1,1),new r("erá",-1,1),new r("irá",-1,1)],W=[new r("a",-1,1),new r("i",-1,1),new r("o",-1,1),new r("os",-1,1),new r("á",-1,1),new r("í",-1,1),new r("ó",-1,1)],L=[new r("e",-1,1),new r("ç",-1,2),new r("é",-1,1),new r("ê",-1,1)],y=[17,65,16,0,0,0,0,0,0,0,0,0,0,0,0,0,3,19,12,2],z=new s;this.setCurrent=function(e){z.setCurrent(e)},this.getCurrent=function(){return z.getCurrent()},this.stem=function(){var r=z.cursor;return e(),z.cursor=r,a(),z.limit_backward=r,z.cursor=z.limit,_(),z.cursor=z.limit,p(),z.cursor=z.limit_backward,u(),!0}};return function(e){return"function"==typeof e.update?e.update(function(e){return n.setCurrent(e),n.stem(),n.getCurrent()}):(n.setCurrent(e),n.stem(),n.getCurrent())}}(),e.Pipeline.registerFunction(e.pt.stemmer,"stemmer-pt"),e.pt.stopWordFilter=e.generateStopWordFilter("a ao aos aquela aquelas aquele aqueles aquilo as até com como da das de dela delas dele deles depois do dos e ela elas ele eles em entre era eram essa essas esse esses esta estamos estas estava estavam este esteja estejam estejamos estes esteve estive estivemos estiver estivera estiveram estiverem estivermos estivesse estivessem estivéramos estivéssemos estou está estávamos estão eu foi fomos for fora foram forem formos fosse fossem fui fôramos fôssemos haja hajam hajamos havemos hei houve houvemos houver houvera houveram houverei houverem houveremos houveria houveriam houvermos houverá houverão houveríamos houvesse houvessem houvéramos houvéssemos há hão isso isto já lhe lhes mais mas me mesmo meu meus minha minhas muito na nas nem no nos nossa nossas nosso nossos num numa não nós o os ou para pela pelas pelo pelos por qual quando que quem se seja sejam sejamos sem serei seremos seria seriam será serão seríamos seu seus somos sou sua suas são só também te tem temos tenha tenham tenhamos tenho terei teremos teria teriam terá terão teríamos teu teus teve tinha tinham tive tivemos tiver tivera tiveram tiverem tivermos tivesse tivessem tivéramos tivéssemos tu tua tuas tém tínhamos um uma você vocês vos à às éramos".split(" ")),e.Pipeline.registerFunction(e.pt.stopWordFilter,"stopWordFilter-pt")}}); \ No newline at end of file diff --git a/v0.13.6/assets/javascripts/lunr/min/lunr.ro.min.js b/v0.13.6/assets/javascripts/lunr/min/lunr.ro.min.js new file mode 100644 index 0000000000..7277140181 --- /dev/null +++ b/v0.13.6/assets/javascripts/lunr/min/lunr.ro.min.js @@ -0,0 +1,18 @@ +/*! + * Lunr languages, `Romanian` language + * https://github.com/MihaiValentin/lunr-languages + * + * Copyright 2014, Mihai Valentin + * http://www.mozilla.org/MPL/ + */ +/*! + * based on + * Snowball JavaScript Library v0.3 + * http://code.google.com/p/urim/ + * http://snowball.tartarus.org/ + * + * Copyright 2010, Oleg Mazko + * http://www.mozilla.org/MPL/ + */ + +!function(e,i){"function"==typeof define&&define.amd?define(i):"object"==typeof exports?module.exports=i():i()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.ro=function(){this.pipeline.reset(),this.pipeline.add(e.ro.trimmer,e.ro.stopWordFilter,e.ro.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.ro.stemmer))},e.ro.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",e.ro.trimmer=e.trimmerSupport.generateTrimmer(e.ro.wordCharacters),e.Pipeline.registerFunction(e.ro.trimmer,"trimmer-ro"),e.ro.stemmer=function(){var i=e.stemmerSupport.Among,r=e.stemmerSupport.SnowballProgram,n=new function(){function e(e,i){L.eq_s(1,e)&&(L.ket=L.cursor,L.in_grouping(W,97,259)&&L.slice_from(i))}function n(){for(var i,r;;){if(i=L.cursor,L.in_grouping(W,97,259)&&(r=L.cursor,L.bra=r,e("u","U"),L.cursor=r,e("i","I")),L.cursor=i,L.cursor>=L.limit)break;L.cursor++}}function t(){if(L.out_grouping(W,97,259)){for(;!L.in_grouping(W,97,259);){if(L.cursor>=L.limit)return!0;L.cursor++}return!1}return!0}function a(){if(L.in_grouping(W,97,259))for(;!L.out_grouping(W,97,259);){if(L.cursor>=L.limit)return!0;L.cursor++}return!1}function o(){var e,i,r=L.cursor;if(L.in_grouping(W,97,259)){if(e=L.cursor,!t())return void(h=L.cursor);if(L.cursor=e,!a())return void(h=L.cursor)}L.cursor=r,L.out_grouping(W,97,259)&&(i=L.cursor,t()&&(L.cursor=i,L.in_grouping(W,97,259)&&L.cursor=L.limit)return!1;L.cursor++}for(;!L.out_grouping(W,97,259);){if(L.cursor>=L.limit)return!1;L.cursor++}return!0}function c(){var e=L.cursor;h=L.limit,k=h,g=h,o(),L.cursor=e,u()&&(k=L.cursor,u()&&(g=L.cursor))}function s(){for(var e;;){if(L.bra=L.cursor,e=L.find_among(z,3))switch(L.ket=L.cursor,e){case 1:L.slice_from("i");continue;case 2:L.slice_from("u");continue;case 3:if(L.cursor>=L.limit)break;L.cursor++;continue}break}}function w(){return h<=L.cursor}function m(){return k<=L.cursor}function l(){return g<=L.cursor}function f(){var e,i;if(L.ket=L.cursor,(e=L.find_among_b(C,16))&&(L.bra=L.cursor,m()))switch(e){case 1:L.slice_del();break;case 2:L.slice_from("a");break;case 3:L.slice_from("e");break;case 4:L.slice_from("i");break;case 5:i=L.limit-L.cursor,L.eq_s_b(2,"ab")||(L.cursor=L.limit-i,L.slice_from("i"));break;case 6:L.slice_from("at");break;case 7:L.slice_from("aţi")}}function p(){var e,i=L.limit-L.cursor;if(L.ket=L.cursor,(e=L.find_among_b(P,46))&&(L.bra=L.cursor,m())){switch(e){case 1:L.slice_from("abil");break;case 2:L.slice_from("ibil");break;case 3:L.slice_from("iv");break;case 4:L.slice_from("ic");break;case 5:L.slice_from("at");break;case 6:L.slice_from("it")}return _=!0,L.cursor=L.limit-i,!0}return!1}function d(){var e,i;for(_=!1;;)if(i=L.limit-L.cursor,!p()){L.cursor=L.limit-i;break}if(L.ket=L.cursor,(e=L.find_among_b(F,62))&&(L.bra=L.cursor,l())){switch(e){case 1:L.slice_del();break;case 2:L.eq_s_b(1,"ţ")&&(L.bra=L.cursor,L.slice_from("t"));break;case 3:L.slice_from("ist")}_=!0}}function b(){var e,i,r;if(L.cursor>=h){if(i=L.limit_backward,L.limit_backward=h,L.ket=L.cursor,e=L.find_among_b(q,94))switch(L.bra=L.cursor,e){case 1:if(r=L.limit-L.cursor,!L.out_grouping_b(W,97,259)&&(L.cursor=L.limit-r,!L.eq_s_b(1,"u")))break;case 2:L.slice_del()}L.limit_backward=i}}function v(){var e;L.ket=L.cursor,(e=L.find_among_b(S,5))&&(L.bra=L.cursor,w()&&1==e&&L.slice_del())}var _,g,k,h,z=[new i("",-1,3),new i("I",0,1),new i("U",0,2)],C=[new i("ea",-1,3),new i("aţia",-1,7),new i("aua",-1,2),new i("iua",-1,4),new i("aţie",-1,7),new i("ele",-1,3),new i("ile",-1,5),new i("iile",6,4),new i("iei",-1,4),new i("atei",-1,6),new i("ii",-1,4),new i("ului",-1,1),new i("ul",-1,1),new i("elor",-1,3),new i("ilor",-1,4),new i("iilor",14,4)],P=[new i("icala",-1,4),new i("iciva",-1,4),new i("ativa",-1,5),new i("itiva",-1,6),new i("icale",-1,4),new i("aţiune",-1,5),new i("iţiune",-1,6),new i("atoare",-1,5),new i("itoare",-1,6),new i("ătoare",-1,5),new i("icitate",-1,4),new i("abilitate",-1,1),new i("ibilitate",-1,2),new i("ivitate",-1,3),new i("icive",-1,4),new i("ative",-1,5),new i("itive",-1,6),new i("icali",-1,4),new i("atori",-1,5),new i("icatori",18,4),new i("itori",-1,6),new i("ători",-1,5),new i("icitati",-1,4),new i("abilitati",-1,1),new i("ivitati",-1,3),new i("icivi",-1,4),new i("ativi",-1,5),new i("itivi",-1,6),new i("icităi",-1,4),new i("abilităi",-1,1),new i("ivităi",-1,3),new i("icităţi",-1,4),new i("abilităţi",-1,1),new i("ivităţi",-1,3),new i("ical",-1,4),new i("ator",-1,5),new i("icator",35,4),new i("itor",-1,6),new i("ător",-1,5),new i("iciv",-1,4),new i("ativ",-1,5),new i("itiv",-1,6),new i("icală",-1,4),new i("icivă",-1,4),new i("ativă",-1,5),new i("itivă",-1,6)],F=[new i("ica",-1,1),new i("abila",-1,1),new i("ibila",-1,1),new i("oasa",-1,1),new i("ata",-1,1),new i("ita",-1,1),new i("anta",-1,1),new i("ista",-1,3),new i("uta",-1,1),new i("iva",-1,1),new i("ic",-1,1),new i("ice",-1,1),new i("abile",-1,1),new i("ibile",-1,1),new i("isme",-1,3),new i("iune",-1,2),new i("oase",-1,1),new i("ate",-1,1),new i("itate",17,1),new i("ite",-1,1),new i("ante",-1,1),new i("iste",-1,3),new i("ute",-1,1),new i("ive",-1,1),new i("ici",-1,1),new i("abili",-1,1),new i("ibili",-1,1),new i("iuni",-1,2),new i("atori",-1,1),new i("osi",-1,1),new i("ati",-1,1),new i("itati",30,1),new i("iti",-1,1),new i("anti",-1,1),new i("isti",-1,3),new i("uti",-1,1),new i("işti",-1,3),new i("ivi",-1,1),new i("ităi",-1,1),new i("oşi",-1,1),new i("ităţi",-1,1),new i("abil",-1,1),new i("ibil",-1,1),new i("ism",-1,3),new i("ator",-1,1),new i("os",-1,1),new i("at",-1,1),new i("it",-1,1),new i("ant",-1,1),new i("ist",-1,3),new i("ut",-1,1),new i("iv",-1,1),new i("ică",-1,1),new i("abilă",-1,1),new i("ibilă",-1,1),new i("oasă",-1,1),new i("ată",-1,1),new i("ită",-1,1),new i("antă",-1,1),new i("istă",-1,3),new i("ută",-1,1),new i("ivă",-1,1)],q=[new i("ea",-1,1),new i("ia",-1,1),new i("esc",-1,1),new i("ăsc",-1,1),new i("ind",-1,1),new i("ând",-1,1),new i("are",-1,1),new i("ere",-1,1),new i("ire",-1,1),new i("âre",-1,1),new i("se",-1,2),new i("ase",10,1),new i("sese",10,2),new i("ise",10,1),new i("use",10,1),new i("âse",10,1),new i("eşte",-1,1),new i("ăşte",-1,1),new i("eze",-1,1),new i("ai",-1,1),new i("eai",19,1),new i("iai",19,1),new i("sei",-1,2),new i("eşti",-1,1),new i("ăşti",-1,1),new i("ui",-1,1),new i("ezi",-1,1),new i("âi",-1,1),new i("aşi",-1,1),new i("seşi",-1,2),new i("aseşi",29,1),new i("seseşi",29,2),new i("iseşi",29,1),new i("useşi",29,1),new i("âseşi",29,1),new i("işi",-1,1),new i("uşi",-1,1),new i("âşi",-1,1),new i("aţi",-1,2),new i("eaţi",38,1),new i("iaţi",38,1),new i("eţi",-1,2),new i("iţi",-1,2),new i("âţi",-1,2),new i("arăţi",-1,1),new i("serăţi",-1,2),new i("aserăţi",45,1),new i("seserăţi",45,2),new i("iserăţi",45,1),new i("userăţi",45,1),new i("âserăţi",45,1),new i("irăţi",-1,1),new i("urăţi",-1,1),new i("ârăţi",-1,1),new i("am",-1,1),new i("eam",54,1),new i("iam",54,1),new i("em",-1,2),new i("asem",57,1),new i("sesem",57,2),new i("isem",57,1),new i("usem",57,1),new i("âsem",57,1),new i("im",-1,2),new i("âm",-1,2),new i("ăm",-1,2),new i("arăm",65,1),new i("serăm",65,2),new i("aserăm",67,1),new i("seserăm",67,2),new i("iserăm",67,1),new i("userăm",67,1),new i("âserăm",67,1),new i("irăm",65,1),new i("urăm",65,1),new i("ârăm",65,1),new i("au",-1,1),new i("eau",76,1),new i("iau",76,1),new i("indu",-1,1),new i("ându",-1,1),new i("ez",-1,1),new i("ească",-1,1),new i("ară",-1,1),new i("seră",-1,2),new i("aseră",84,1),new i("seseră",84,2),new i("iseră",84,1),new i("useră",84,1),new i("âseră",84,1),new i("iră",-1,1),new i("ură",-1,1),new i("âră",-1,1),new i("ează",-1,1)],S=[new i("a",-1,1),new i("e",-1,1),new i("ie",1,1),new i("i",-1,1),new i("ă",-1,1)],W=[17,65,16,0,0,0,0,0,0,0,0,0,0,0,0,0,2,32,0,0,4],L=new r;this.setCurrent=function(e){L.setCurrent(e)},this.getCurrent=function(){return L.getCurrent()},this.stem=function(){var e=L.cursor;return n(),L.cursor=e,c(),L.limit_backward=e,L.cursor=L.limit,f(),L.cursor=L.limit,d(),L.cursor=L.limit,_||(L.cursor=L.limit,b(),L.cursor=L.limit),v(),L.cursor=L.limit_backward,s(),!0}};return function(e){return"function"==typeof e.update?e.update(function(e){return n.setCurrent(e),n.stem(),n.getCurrent()}):(n.setCurrent(e),n.stem(),n.getCurrent())}}(),e.Pipeline.registerFunction(e.ro.stemmer,"stemmer-ro"),e.ro.stopWordFilter=e.generateStopWordFilter("acea aceasta această aceea acei aceia acel acela acele acelea acest acesta aceste acestea aceşti aceştia acolo acord acum ai aia aibă aici al ale alea altceva altcineva am ar are asemenea asta astea astăzi asupra au avea avem aveţi azi aş aşadar aţi bine bucur bună ca care caut ce cel ceva chiar cinci cine cineva contra cu cum cumva curând curînd când cât câte câtva câţi cînd cît cîte cîtva cîţi că căci cărei căror cărui către da dacă dar datorită dată dau de deci deja deoarece departe deşi din dinaintea dintr- dintre doi doilea două drept după dă ea ei el ele eram este eu eşti face fata fi fie fiecare fii fim fiu fiţi frumos fără graţie halbă iar ieri la le li lor lui lângă lîngă mai mea mei mele mereu meu mi mie mine mult multă mulţi mulţumesc mâine mîine mă ne nevoie nici nicăieri nimeni nimeri nimic nişte noastre noastră noi noroc nostru nouă noştri nu opt ori oricare orice oricine oricum oricând oricât oricînd oricît oriunde patra patru patrulea pe pentru peste pic poate pot prea prima primul prin puţin puţina puţină până pînă rog sa sale sau se spate spre sub sunt suntem sunteţi sută sînt sîntem sînteţi să săi său ta tale te timp tine toate toată tot totuşi toţi trei treia treilea tu tăi tău un una unde undeva unei uneia unele uneori unii unor unora unu unui unuia unul vi voastre voastră voi vostru vouă voştri vreme vreo vreun vă zece zero zi zice îi îl îmi împotriva în înainte înaintea încotro încât încît între întrucât întrucît îţi ăla ălea ăsta ăstea ăştia şapte şase şi ştiu ţi ţie".split(" ")),e.Pipeline.registerFunction(e.ro.stopWordFilter,"stopWordFilter-ro")}}); \ No newline at end of file diff --git a/v0.13.6/assets/javascripts/lunr/min/lunr.ru.min.js b/v0.13.6/assets/javascripts/lunr/min/lunr.ru.min.js new file mode 100644 index 0000000000..186cc485c2 --- /dev/null +++ b/v0.13.6/assets/javascripts/lunr/min/lunr.ru.min.js @@ -0,0 +1,18 @@ +/*! + * Lunr languages, `Russian` language + * https://github.com/MihaiValentin/lunr-languages + * + * Copyright 2014, Mihai Valentin + * http://www.mozilla.org/MPL/ + */ +/*! + * based on + * Snowball JavaScript Library v0.3 + * http://code.google.com/p/urim/ + * http://snowball.tartarus.org/ + * + * Copyright 2010, Oleg Mazko + * http://www.mozilla.org/MPL/ + */ + +!function(e,n){"function"==typeof define&&define.amd?define(n):"object"==typeof exports?module.exports=n():n()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.ru=function(){this.pipeline.reset(),this.pipeline.add(e.ru.trimmer,e.ru.stopWordFilter,e.ru.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.ru.stemmer))},e.ru.wordCharacters="Ѐ-҄҇-ԯᴫᵸⷠ-ⷿꙀ-ꚟ︮︯",e.ru.trimmer=e.trimmerSupport.generateTrimmer(e.ru.wordCharacters),e.Pipeline.registerFunction(e.ru.trimmer,"trimmer-ru"),e.ru.stemmer=function(){var n=e.stemmerSupport.Among,r=e.stemmerSupport.SnowballProgram,t=new function(){function e(){for(;!W.in_grouping(S,1072,1103);){if(W.cursor>=W.limit)return!1;W.cursor++}return!0}function t(){for(;!W.out_grouping(S,1072,1103);){if(W.cursor>=W.limit)return!1;W.cursor++}return!0}function w(){b=W.limit,_=b,e()&&(b=W.cursor,t()&&e()&&t()&&(_=W.cursor))}function i(){return _<=W.cursor}function u(e,n){var r,t;if(W.ket=W.cursor,r=W.find_among_b(e,n)){switch(W.bra=W.cursor,r){case 1:if(t=W.limit-W.cursor,!W.eq_s_b(1,"а")&&(W.cursor=W.limit-t,!W.eq_s_b(1,"я")))return!1;case 2:W.slice_del()}return!0}return!1}function o(){return u(h,9)}function s(e,n){var r;return W.ket=W.cursor,!!(r=W.find_among_b(e,n))&&(W.bra=W.cursor,1==r&&W.slice_del(),!0)}function c(){return s(g,26)}function m(){return!!c()&&(u(C,8),!0)}function f(){return s(k,2)}function l(){return u(P,46)}function a(){s(v,36)}function p(){var e;W.ket=W.cursor,(e=W.find_among_b(F,2))&&(W.bra=W.cursor,i()&&1==e&&W.slice_del())}function d(){var e;if(W.ket=W.cursor,e=W.find_among_b(q,4))switch(W.bra=W.cursor,e){case 1:if(W.slice_del(),W.ket=W.cursor,!W.eq_s_b(1,"н"))break;W.bra=W.cursor;case 2:if(!W.eq_s_b(1,"н"))break;case 3:W.slice_del()}}var _,b,h=[new n("в",-1,1),new n("ив",0,2),new n("ыв",0,2),new n("вши",-1,1),new n("ивши",3,2),new n("ывши",3,2),new n("вшись",-1,1),new n("ившись",6,2),new n("ывшись",6,2)],g=[new n("ее",-1,1),new n("ие",-1,1),new n("ое",-1,1),new n("ые",-1,1),new n("ими",-1,1),new n("ыми",-1,1),new n("ей",-1,1),new n("ий",-1,1),new n("ой",-1,1),new n("ый",-1,1),new n("ем",-1,1),new n("им",-1,1),new n("ом",-1,1),new n("ым",-1,1),new n("его",-1,1),new n("ого",-1,1),new n("ему",-1,1),new n("ому",-1,1),new n("их",-1,1),new n("ых",-1,1),new n("ею",-1,1),new n("ою",-1,1),new n("ую",-1,1),new n("юю",-1,1),new n("ая",-1,1),new n("яя",-1,1)],C=[new n("ем",-1,1),new n("нн",-1,1),new n("вш",-1,1),new n("ивш",2,2),new n("ывш",2,2),new n("щ",-1,1),new n("ющ",5,1),new n("ующ",6,2)],k=[new n("сь",-1,1),new n("ся",-1,1)],P=[new n("ла",-1,1),new n("ила",0,2),new n("ыла",0,2),new n("на",-1,1),new n("ена",3,2),new n("ете",-1,1),new n("ите",-1,2),new n("йте",-1,1),new n("ейте",7,2),new n("уйте",7,2),new n("ли",-1,1),new n("или",10,2),new n("ыли",10,2),new n("й",-1,1),new n("ей",13,2),new n("уй",13,2),new n("л",-1,1),new n("ил",16,2),new n("ыл",16,2),new n("ем",-1,1),new n("им",-1,2),new n("ым",-1,2),new n("н",-1,1),new n("ен",22,2),new n("ло",-1,1),new n("ило",24,2),new n("ыло",24,2),new n("но",-1,1),new n("ено",27,2),new n("нно",27,1),new n("ет",-1,1),new n("ует",30,2),new n("ит",-1,2),new n("ыт",-1,2),new n("ют",-1,1),new n("уют",34,2),new n("ят",-1,2),new n("ны",-1,1),new n("ены",37,2),new n("ть",-1,1),new n("ить",39,2),new n("ыть",39,2),new n("ешь",-1,1),new n("ишь",-1,2),new n("ю",-1,2),new n("ую",44,2)],v=[new n("а",-1,1),new n("ев",-1,1),new n("ов",-1,1),new n("е",-1,1),new n("ие",3,1),new n("ье",3,1),new n("и",-1,1),new n("еи",6,1),new n("ии",6,1),new n("ами",6,1),new n("ями",6,1),new n("иями",10,1),new n("й",-1,1),new n("ей",12,1),new n("ией",13,1),new n("ий",12,1),new n("ой",12,1),new n("ам",-1,1),new n("ем",-1,1),new n("ием",18,1),new n("ом",-1,1),new n("ям",-1,1),new n("иям",21,1),new n("о",-1,1),new n("у",-1,1),new n("ах",-1,1),new n("ях",-1,1),new n("иях",26,1),new n("ы",-1,1),new n("ь",-1,1),new n("ю",-1,1),new n("ию",30,1),new n("ью",30,1),new n("я",-1,1),new n("ия",33,1),new n("ья",33,1)],F=[new n("ост",-1,1),new n("ость",-1,1)],q=[new n("ейше",-1,1),new n("н",-1,2),new n("ейш",-1,1),new n("ь",-1,3)],S=[33,65,8,232],W=new r;this.setCurrent=function(e){W.setCurrent(e)},this.getCurrent=function(){return W.getCurrent()},this.stem=function(){return w(),W.cursor=W.limit,!(W.cursor=i&&(e-=i,t[e>>3]&1<<(7&e)))return this.cursor++,!0}return!1},in_grouping_b:function(t,i,s){if(this.cursor>this.limit_backward){var e=r.charCodeAt(this.cursor-1);if(e<=s&&e>=i&&(e-=i,t[e>>3]&1<<(7&e)))return this.cursor--,!0}return!1},out_grouping:function(t,i,s){if(this.cursors||e>3]&1<<(7&e)))return this.cursor++,!0}return!1},out_grouping_b:function(t,i,s){if(this.cursor>this.limit_backward){var e=r.charCodeAt(this.cursor-1);if(e>s||e>3]&1<<(7&e)))return this.cursor--,!0}return!1},eq_s:function(t,i){if(this.limit-this.cursor>1),f=0,l=o0||e==s||c)break;c=!0}}for(;;){var _=t[s];if(o>=_.s_size){if(this.cursor=n+_.s_size,!_.method)return _.result;var b=_.method();if(this.cursor=n+_.s_size,b)return _.result}if((s=_.substring_i)<0)return 0}},find_among_b:function(t,i){for(var s=0,e=i,n=this.cursor,u=this.limit_backward,o=0,h=0,c=!1;;){for(var a=s+(e-s>>1),f=0,l=o=0;m--){if(n-l==u){f=-1;break}if(f=r.charCodeAt(n-1-l)-_.s[m])break;l++}if(f<0?(e=a,h=l):(s=a,o=l),e-s<=1){if(s>0||e==s||c)break;c=!0}}for(;;){var _=t[s];if(o>=_.s_size){if(this.cursor=n-_.s_size,!_.method)return _.result;var b=_.method();if(this.cursor=n-_.s_size,b)return _.result}if((s=_.substring_i)<0)return 0}},replace_s:function(t,i,s){var e=s.length-(i-t),n=r.substring(0,t),u=r.substring(i);return r=n+s+u,this.limit+=e,this.cursor>=i?this.cursor+=e:this.cursor>t&&(this.cursor=t),e},slice_check:function(){if(this.bra<0||this.bra>this.ket||this.ket>this.limit||this.limit>r.length)throw"faulty slice operation"},slice_from:function(r){this.slice_check(),this.replace_s(this.bra,this.ket,r)},slice_del:function(){this.slice_from("")},insert:function(r,t,i){var s=this.replace_s(r,t,i);r<=this.bra&&(this.bra+=s),r<=this.ket&&(this.ket+=s)},slice_to:function(){return this.slice_check(),r.substring(this.bra,this.ket)},eq_v_b:function(r){return this.eq_s_b(r.length,r)}}}},r.trimmerSupport={generateTrimmer:function(r){var t=new RegExp("^[^"+r+"]+"),i=new RegExp("[^"+r+"]+$");return function(r){return"function"==typeof r.update?r.update(function(r){return r.replace(t,"").replace(i,"")}):r.replace(t,"").replace(i,"")}}}}}); \ No newline at end of file diff --git a/v0.13.6/assets/javascripts/lunr/min/lunr.sv.min.js b/v0.13.6/assets/javascripts/lunr/min/lunr.sv.min.js new file mode 100644 index 0000000000..3e5eb64000 --- /dev/null +++ b/v0.13.6/assets/javascripts/lunr/min/lunr.sv.min.js @@ -0,0 +1,18 @@ +/*! + * Lunr languages, `Swedish` language + * https://github.com/MihaiValentin/lunr-languages + * + * Copyright 2014, Mihai Valentin + * http://www.mozilla.org/MPL/ + */ +/*! + * based on + * Snowball JavaScript Library v0.3 + * http://code.google.com/p/urim/ + * http://snowball.tartarus.org/ + * + * Copyright 2010, Oleg Mazko + * http://www.mozilla.org/MPL/ + */ + +!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.sv=function(){this.pipeline.reset(),this.pipeline.add(e.sv.trimmer,e.sv.stopWordFilter,e.sv.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.sv.stemmer))},e.sv.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",e.sv.trimmer=e.trimmerSupport.generateTrimmer(e.sv.wordCharacters),e.Pipeline.registerFunction(e.sv.trimmer,"trimmer-sv"),e.sv.stemmer=function(){var r=e.stemmerSupport.Among,n=e.stemmerSupport.SnowballProgram,t=new function(){function e(){var e,r=w.cursor+3;if(o=w.limit,0<=r||r<=w.limit){for(a=r;;){if(e=w.cursor,w.in_grouping(l,97,246)){w.cursor=e;break}if(w.cursor=e,w.cursor>=w.limit)return;w.cursor++}for(;!w.out_grouping(l,97,246);){if(w.cursor>=w.limit)return;w.cursor++}o=w.cursor,o=o&&(w.limit_backward=o,w.cursor=w.limit,w.ket=w.cursor,e=w.find_among_b(u,37),w.limit_backward=r,e))switch(w.bra=w.cursor,e){case 1:w.slice_del();break;case 2:w.in_grouping_b(d,98,121)&&w.slice_del()}}function i(){var e=w.limit_backward;w.cursor>=o&&(w.limit_backward=o,w.cursor=w.limit,w.find_among_b(c,7)&&(w.cursor=w.limit,w.ket=w.cursor,w.cursor>w.limit_backward&&(w.bra=--w.cursor,w.slice_del())),w.limit_backward=e)}function s(){var e,r;if(w.cursor>=o){if(r=w.limit_backward,w.limit_backward=o,w.cursor=w.limit,w.ket=w.cursor,e=w.find_among_b(m,5))switch(w.bra=w.cursor,e){case 1:w.slice_del();break;case 2:w.slice_from("lös");break;case 3:w.slice_from("full")}w.limit_backward=r}}var a,o,u=[new r("a",-1,1),new r("arna",0,1),new r("erna",0,1),new r("heterna",2,1),new r("orna",0,1),new r("ad",-1,1),new r("e",-1,1),new r("ade",6,1),new r("ande",6,1),new r("arne",6,1),new r("are",6,1),new r("aste",6,1),new r("en",-1,1),new r("anden",12,1),new r("aren",12,1),new r("heten",12,1),new r("ern",-1,1),new r("ar",-1,1),new r("er",-1,1),new r("heter",18,1),new r("or",-1,1),new r("s",-1,2),new r("as",21,1),new r("arnas",22,1),new r("ernas",22,1),new r("ornas",22,1),new r("es",21,1),new r("ades",26,1),new r("andes",26,1),new r("ens",21,1),new r("arens",29,1),new r("hetens",29,1),new r("erns",21,1),new r("at",-1,1),new r("andet",-1,1),new r("het",-1,1),new r("ast",-1,1)],c=[new r("dd",-1,-1),new r("gd",-1,-1),new r("nn",-1,-1),new r("dt",-1,-1),new r("gt",-1,-1),new r("kt",-1,-1),new r("tt",-1,-1)],m=[new r("ig",-1,1),new r("lig",0,1),new r("els",-1,1),new r("fullt",-1,3),new r("löst",-1,2)],l=[17,65,16,1,0,0,0,0,0,0,0,0,0,0,0,0,24,0,32],d=[119,127,149],w=new n;this.setCurrent=function(e){w.setCurrent(e)},this.getCurrent=function(){return w.getCurrent()},this.stem=function(){var r=w.cursor;return e(),w.limit_backward=r,w.cursor=w.limit,t(),w.cursor=w.limit,i(),w.cursor=w.limit,s(),!0}};return function(e){return"function"==typeof e.update?e.update(function(e){return t.setCurrent(e),t.stem(),t.getCurrent()}):(t.setCurrent(e),t.stem(),t.getCurrent())}}(),e.Pipeline.registerFunction(e.sv.stemmer,"stemmer-sv"),e.sv.stopWordFilter=e.generateStopWordFilter("alla allt att av blev bli blir blivit de dem den denna deras dess dessa det detta dig din dina ditt du där då efter ej eller en er era ert ett från för ha hade han hans har henne hennes hon honom hur här i icke ingen inom inte jag ju kan kunde man med mellan men mig min mina mitt mot mycket ni nu när någon något några och om oss på samma sedan sig sin sina sitta själv skulle som så sådan sådana sådant till under upp ut utan vad var vara varför varit varje vars vart vem vi vid vilka vilkas vilken vilket vår våra vårt än är åt över".split(" ")),e.Pipeline.registerFunction(e.sv.stopWordFilter,"stopWordFilter-sv")}}); \ No newline at end of file diff --git a/v0.13.6/assets/javascripts/lunr/min/lunr.th.min.js b/v0.13.6/assets/javascripts/lunr/min/lunr.th.min.js new file mode 100644 index 0000000000..dee3aac6e5 --- /dev/null +++ b/v0.13.6/assets/javascripts/lunr/min/lunr.th.min.js @@ -0,0 +1 @@ +!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");var r="2"==e.version[0];e.th=function(){this.pipeline.reset(),this.pipeline.add(e.th.trimmer),r?this.tokenizer=e.th.tokenizer:(e.tokenizer&&(e.tokenizer=e.th.tokenizer),this.tokenizerFn&&(this.tokenizerFn=e.th.tokenizer))},e.th.wordCharacters="[฀-๿]",e.th.trimmer=e.trimmerSupport.generateTrimmer(e.th.wordCharacters),e.Pipeline.registerFunction(e.th.trimmer,"trimmer-th");var t=e.wordcut;t.init(),e.th.tokenizer=function(i){if(!arguments.length||null==i||void 0==i)return[];if(Array.isArray(i))return i.map(function(t){return r?new e.Token(t):t});var n=i.toString().replace(/^\s+/,"");return t.cut(n).split("|")}}}); \ No newline at end of file diff --git a/v0.13.6/assets/javascripts/lunr/min/lunr.tr.min.js b/v0.13.6/assets/javascripts/lunr/min/lunr.tr.min.js new file mode 100644 index 0000000000..563f6ec1f5 --- /dev/null +++ b/v0.13.6/assets/javascripts/lunr/min/lunr.tr.min.js @@ -0,0 +1,18 @@ +/*! + * Lunr languages, `Turkish` language + * https://github.com/MihaiValentin/lunr-languages + * + * Copyright 2014, Mihai Valentin + * http://www.mozilla.org/MPL/ + */ +/*! + * based on + * Snowball JavaScript Library v0.3 + * http://code.google.com/p/urim/ + * http://snowball.tartarus.org/ + * + * Copyright 2010, Oleg Mazko + * http://www.mozilla.org/MPL/ + */ + +!function(r,i){"function"==typeof define&&define.amd?define(i):"object"==typeof exports?module.exports=i():i()(r.lunr)}(this,function(){return function(r){if(void 0===r)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===r.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");r.tr=function(){this.pipeline.reset(),this.pipeline.add(r.tr.trimmer,r.tr.stopWordFilter,r.tr.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(r.tr.stemmer))},r.tr.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",r.tr.trimmer=r.trimmerSupport.generateTrimmer(r.tr.wordCharacters),r.Pipeline.registerFunction(r.tr.trimmer,"trimmer-tr"),r.tr.stemmer=function(){var i=r.stemmerSupport.Among,e=r.stemmerSupport.SnowballProgram,n=new function(){function r(r,i,e){for(;;){var n=Dr.limit-Dr.cursor;if(Dr.in_grouping_b(r,i,e)){Dr.cursor=Dr.limit-n;break}if(Dr.cursor=Dr.limit-n,Dr.cursor<=Dr.limit_backward)return!1;Dr.cursor--}return!0}function n(){var i,e;i=Dr.limit-Dr.cursor,r(Wr,97,305);for(var n=0;nDr.limit_backward&&(Dr.cursor--,e=Dr.limit-Dr.cursor,i()))?(Dr.cursor=Dr.limit-e,!0):(Dr.cursor=Dr.limit-n,r()?(Dr.cursor=Dr.limit-n,!1):(Dr.cursor=Dr.limit-n,!(Dr.cursor<=Dr.limit_backward)&&(Dr.cursor--,!!i()&&(Dr.cursor=Dr.limit-n,!0))))}function u(r){return t(r,function(){return Dr.in_grouping_b(Wr,97,305)})}function o(){return u(function(){return Dr.eq_s_b(1,"n")})}function s(){return u(function(){return Dr.eq_s_b(1,"s")})}function c(){return u(function(){return Dr.eq_s_b(1,"y")})}function l(){return t(function(){return Dr.in_grouping_b(Lr,105,305)},function(){return Dr.out_grouping_b(Wr,97,305)})}function a(){return Dr.find_among_b(ur,10)&&l()}function m(){return n()&&Dr.in_grouping_b(Lr,105,305)&&s()}function d(){return Dr.find_among_b(or,2)}function f(){return n()&&Dr.in_grouping_b(Lr,105,305)&&c()}function b(){return n()&&Dr.find_among_b(sr,4)}function w(){return n()&&Dr.find_among_b(cr,4)&&o()}function _(){return n()&&Dr.find_among_b(lr,2)&&c()}function k(){return n()&&Dr.find_among_b(ar,2)}function p(){return n()&&Dr.find_among_b(mr,4)}function g(){return n()&&Dr.find_among_b(dr,2)}function y(){return n()&&Dr.find_among_b(fr,4)}function z(){return n()&&Dr.find_among_b(br,2)}function v(){return n()&&Dr.find_among_b(wr,2)&&c()}function h(){return Dr.eq_s_b(2,"ki")}function q(){return n()&&Dr.find_among_b(_r,2)&&o()}function C(){return n()&&Dr.find_among_b(kr,4)&&c()}function P(){return n()&&Dr.find_among_b(pr,4)}function F(){return n()&&Dr.find_among_b(gr,4)&&c()}function S(){return Dr.find_among_b(yr,4)}function W(){return n()&&Dr.find_among_b(zr,2)}function L(){return n()&&Dr.find_among_b(vr,4)}function x(){return n()&&Dr.find_among_b(hr,8)}function A(){return Dr.find_among_b(qr,2)}function E(){return n()&&Dr.find_among_b(Cr,32)&&c()}function j(){return Dr.find_among_b(Pr,8)&&c()}function T(){return n()&&Dr.find_among_b(Fr,4)&&c()}function Z(){return Dr.eq_s_b(3,"ken")&&c()}function B(){var r=Dr.limit-Dr.cursor;return!(T()||(Dr.cursor=Dr.limit-r,E()||(Dr.cursor=Dr.limit-r,j()||(Dr.cursor=Dr.limit-r,Z()))))}function D(){if(A()){var r=Dr.limit-Dr.cursor;if(S()||(Dr.cursor=Dr.limit-r,W()||(Dr.cursor=Dr.limit-r,C()||(Dr.cursor=Dr.limit-r,P()||(Dr.cursor=Dr.limit-r,F()||(Dr.cursor=Dr.limit-r))))),T())return!1}return!0}function G(){if(W()){Dr.bra=Dr.cursor,Dr.slice_del();var r=Dr.limit-Dr.cursor;return Dr.ket=Dr.cursor,x()||(Dr.cursor=Dr.limit-r,E()||(Dr.cursor=Dr.limit-r,j()||(Dr.cursor=Dr.limit-r,T()||(Dr.cursor=Dr.limit-r)))),nr=!1,!1}return!0}function H(){if(!L())return!0;var r=Dr.limit-Dr.cursor;return!E()&&(Dr.cursor=Dr.limit-r,!j())}function I(){var r,i=Dr.limit-Dr.cursor;return!(S()||(Dr.cursor=Dr.limit-i,F()||(Dr.cursor=Dr.limit-i,P()||(Dr.cursor=Dr.limit-i,C()))))||(Dr.bra=Dr.cursor,Dr.slice_del(),r=Dr.limit-Dr.cursor,Dr.ket=Dr.cursor,T()||(Dr.cursor=Dr.limit-r),!1)}function J(){var r,i=Dr.limit-Dr.cursor;if(Dr.ket=Dr.cursor,nr=!0,B()&&(Dr.cursor=Dr.limit-i,D()&&(Dr.cursor=Dr.limit-i,G()&&(Dr.cursor=Dr.limit-i,H()&&(Dr.cursor=Dr.limit-i,I()))))){if(Dr.cursor=Dr.limit-i,!x())return;Dr.bra=Dr.cursor,Dr.slice_del(),Dr.ket=Dr.cursor,r=Dr.limit-Dr.cursor,S()||(Dr.cursor=Dr.limit-r,W()||(Dr.cursor=Dr.limit-r,C()||(Dr.cursor=Dr.limit-r,P()||(Dr.cursor=Dr.limit-r,F()||(Dr.cursor=Dr.limit-r))))),T()||(Dr.cursor=Dr.limit-r)}Dr.bra=Dr.cursor,Dr.slice_del()}function K(){var r,i,e,n;if(Dr.ket=Dr.cursor,h()){if(r=Dr.limit-Dr.cursor,p())return Dr.bra=Dr.cursor,Dr.slice_del(),i=Dr.limit-Dr.cursor,Dr.ket=Dr.cursor,W()?(Dr.bra=Dr.cursor,Dr.slice_del(),K()):(Dr.cursor=Dr.limit-i,a()&&(Dr.bra=Dr.cursor,Dr.slice_del(),Dr.ket=Dr.cursor,W()&&(Dr.bra=Dr.cursor,Dr.slice_del(),K()))),!0;if(Dr.cursor=Dr.limit-r,w()){if(Dr.bra=Dr.cursor,Dr.slice_del(),Dr.ket=Dr.cursor,e=Dr.limit-Dr.cursor,d())Dr.bra=Dr.cursor,Dr.slice_del();else{if(Dr.cursor=Dr.limit-e,Dr.ket=Dr.cursor,!a()&&(Dr.cursor=Dr.limit-e,!m()&&(Dr.cursor=Dr.limit-e,!K())))return!0;Dr.bra=Dr.cursor,Dr.slice_del(),Dr.ket=Dr.cursor,W()&&(Dr.bra=Dr.cursor,Dr.slice_del(),K())}return!0}if(Dr.cursor=Dr.limit-r,g()){if(n=Dr.limit-Dr.cursor,d())Dr.bra=Dr.cursor,Dr.slice_del();else if(Dr.cursor=Dr.limit-n,m())Dr.bra=Dr.cursor,Dr.slice_del(),Dr.ket=Dr.cursor,W()&&(Dr.bra=Dr.cursor,Dr.slice_del(),K());else if(Dr.cursor=Dr.limit-n,!K())return!1;return!0}}return!1}function M(r){if(Dr.ket=Dr.cursor,!g()&&(Dr.cursor=Dr.limit-r,!k()))return!1;var i=Dr.limit-Dr.cursor;if(d())Dr.bra=Dr.cursor,Dr.slice_del();else if(Dr.cursor=Dr.limit-i,m())Dr.bra=Dr.cursor,Dr.slice_del(),Dr.ket=Dr.cursor,W()&&(Dr.bra=Dr.cursor,Dr.slice_del(),K());else if(Dr.cursor=Dr.limit-i,!K())return!1;return!0}function N(r){if(Dr.ket=Dr.cursor,!z()&&(Dr.cursor=Dr.limit-r,!b()))return!1;var i=Dr.limit-Dr.cursor;return!(!m()&&(Dr.cursor=Dr.limit-i,!d()))&&(Dr.bra=Dr.cursor,Dr.slice_del(),Dr.ket=Dr.cursor,W()&&(Dr.bra=Dr.cursor,Dr.slice_del(),K()),!0)}function O(){var r,i=Dr.limit-Dr.cursor;return Dr.ket=Dr.cursor,!(!w()&&(Dr.cursor=Dr.limit-i,!v()))&&(Dr.bra=Dr.cursor,Dr.slice_del(),r=Dr.limit-Dr.cursor,Dr.ket=Dr.cursor,!(!W()||(Dr.bra=Dr.cursor,Dr.slice_del(),!K()))||(Dr.cursor=Dr.limit-r,Dr.ket=Dr.cursor,!(a()||(Dr.cursor=Dr.limit-r,m()||(Dr.cursor=Dr.limit-r,K())))||(Dr.bra=Dr.cursor,Dr.slice_del(),Dr.ket=Dr.cursor,W()&&(Dr.bra=Dr.cursor,Dr.slice_del(),K()),!0)))}function Q(){var r,i,e=Dr.limit-Dr.cursor;if(Dr.ket=Dr.cursor,!p()&&(Dr.cursor=Dr.limit-e,!f()&&(Dr.cursor=Dr.limit-e,!_())))return!1;if(Dr.bra=Dr.cursor,Dr.slice_del(),Dr.ket=Dr.cursor,r=Dr.limit-Dr.cursor,a())Dr.bra=Dr.cursor,Dr.slice_del(),i=Dr.limit-Dr.cursor,Dr.ket=Dr.cursor,W()||(Dr.cursor=Dr.limit-i);else if(Dr.cursor=Dr.limit-r,!W())return!0;return Dr.bra=Dr.cursor,Dr.slice_del(),Dr.ket=Dr.cursor,K(),!0}function R(){var r,i,e=Dr.limit-Dr.cursor;if(Dr.ket=Dr.cursor,W())return Dr.bra=Dr.cursor,Dr.slice_del(),void K();if(Dr.cursor=Dr.limit-e,Dr.ket=Dr.cursor,q())if(Dr.bra=Dr.cursor,Dr.slice_del(),r=Dr.limit-Dr.cursor,Dr.ket=Dr.cursor,d())Dr.bra=Dr.cursor,Dr.slice_del();else{if(Dr.cursor=Dr.limit-r,Dr.ket=Dr.cursor,!a()&&(Dr.cursor=Dr.limit-r,!m())){if(Dr.cursor=Dr.limit-r,Dr.ket=Dr.cursor,!W())return;if(Dr.bra=Dr.cursor,Dr.slice_del(),!K())return}Dr.bra=Dr.cursor,Dr.slice_del(),Dr.ket=Dr.cursor,W()&&(Dr.bra=Dr.cursor,Dr.slice_del(),K())}else if(Dr.cursor=Dr.limit-e,!M(e)&&(Dr.cursor=Dr.limit-e,!N(e))){if(Dr.cursor=Dr.limit-e,Dr.ket=Dr.cursor,y())return Dr.bra=Dr.cursor,Dr.slice_del(),Dr.ket=Dr.cursor,i=Dr.limit-Dr.cursor,void(a()?(Dr.bra=Dr.cursor,Dr.slice_del(),Dr.ket=Dr.cursor,W()&&(Dr.bra=Dr.cursor,Dr.slice_del(),K())):(Dr.cursor=Dr.limit-i,W()?(Dr.bra=Dr.cursor,Dr.slice_del(),K()):(Dr.cursor=Dr.limit-i,K())));if(Dr.cursor=Dr.limit-e,!O()){if(Dr.cursor=Dr.limit-e,d())return Dr.bra=Dr.cursor,void Dr.slice_del();Dr.cursor=Dr.limit-e,K()||(Dr.cursor=Dr.limit-e,Q()||(Dr.cursor=Dr.limit-e,Dr.ket=Dr.cursor,(a()||(Dr.cursor=Dr.limit-e,m()))&&(Dr.bra=Dr.cursor,Dr.slice_del(),Dr.ket=Dr.cursor,W()&&(Dr.bra=Dr.cursor,Dr.slice_del(),K()))))}}}function U(){var r;if(Dr.ket=Dr.cursor,r=Dr.find_among_b(Sr,4))switch(Dr.bra=Dr.cursor,r){case 1:Dr.slice_from("p");break;case 2:Dr.slice_from("ç");break;case 3:Dr.slice_from("t");break;case 4:Dr.slice_from("k")}}function V(){for(;;){var r=Dr.limit-Dr.cursor;if(Dr.in_grouping_b(Wr,97,305)){Dr.cursor=Dr.limit-r;break}if(Dr.cursor=Dr.limit-r,Dr.cursor<=Dr.limit_backward)return!1;Dr.cursor--}return!0}function X(r,i,e){if(Dr.cursor=Dr.limit-r,V()){var n=Dr.limit-Dr.cursor;if(!Dr.eq_s_b(1,i)&&(Dr.cursor=Dr.limit-n,!Dr.eq_s_b(1,e)))return!0;Dr.cursor=Dr.limit-r;var t=Dr.cursor;return Dr.insert(Dr.cursor,Dr.cursor,e),Dr.cursor=t,!1}return!0}function Y(){var r=Dr.limit-Dr.cursor;(Dr.eq_s_b(1,"d")||(Dr.cursor=Dr.limit-r,Dr.eq_s_b(1,"g")))&&X(r,"a","ı")&&X(r,"e","i")&&X(r,"o","u")&&X(r,"ö","ü")}function $(){for(var r,i=Dr.cursor,e=2;;){for(r=Dr.cursor;!Dr.in_grouping(Wr,97,305);){if(Dr.cursor>=Dr.limit)return Dr.cursor=r,!(e>0)&&(Dr.cursor=i,!0);Dr.cursor++}e--}}function rr(r,i,e){for(;!Dr.eq_s(i,e);){if(Dr.cursor>=Dr.limit)return!0;Dr.cursor++}return(tr=i)!=Dr.limit||(Dr.cursor=r,!1)}function ir(){var r=Dr.cursor;return!rr(r,2,"ad")||(Dr.cursor=r,!rr(r,5,"soyad"))}function er(){var r=Dr.cursor;return!ir()&&(Dr.limit_backward=r,Dr.cursor=Dr.limit,Y(),Dr.cursor=Dr.limit,U(),!0)}var nr,tr,ur=[new i("m",-1,-1),new i("n",-1,-1),new i("miz",-1,-1),new i("niz",-1,-1),new i("muz",-1,-1),new i("nuz",-1,-1),new i("müz",-1,-1),new i("nüz",-1,-1),new i("mız",-1,-1),new i("nız",-1,-1)],or=[new i("leri",-1,-1),new i("ları",-1,-1)],sr=[new i("ni",-1,-1),new i("nu",-1,-1),new i("nü",-1,-1),new i("nı",-1,-1)],cr=[new i("in",-1,-1),new i("un",-1,-1),new i("ün",-1,-1),new i("ın",-1,-1)],lr=[new i("a",-1,-1),new i("e",-1,-1)],ar=[new i("na",-1,-1),new i("ne",-1,-1)],mr=[new i("da",-1,-1),new i("ta",-1,-1),new i("de",-1,-1),new i("te",-1,-1)],dr=[new i("nda",-1,-1),new i("nde",-1,-1)],fr=[new i("dan",-1,-1),new i("tan",-1,-1),new i("den",-1,-1),new i("ten",-1,-1)],br=[new i("ndan",-1,-1),new i("nden",-1,-1)],wr=[new i("la",-1,-1),new i("le",-1,-1)],_r=[new i("ca",-1,-1),new i("ce",-1,-1)],kr=[new i("im",-1,-1),new i("um",-1,-1),new i("üm",-1,-1),new i("ım",-1,-1)],pr=[new i("sin",-1,-1),new i("sun",-1,-1),new i("sün",-1,-1),new i("sın",-1,-1)],gr=[new i("iz",-1,-1),new i("uz",-1,-1),new i("üz",-1,-1),new i("ız",-1,-1)],yr=[new i("siniz",-1,-1),new i("sunuz",-1,-1),new i("sünüz",-1,-1),new i("sınız",-1,-1)],zr=[new i("lar",-1,-1),new i("ler",-1,-1)],vr=[new i("niz",-1,-1),new i("nuz",-1,-1),new i("nüz",-1,-1),new i("nız",-1,-1)],hr=[new i("dir",-1,-1),new i("tir",-1,-1),new i("dur",-1,-1),new i("tur",-1,-1),new i("dür",-1,-1),new i("tür",-1,-1),new i("dır",-1,-1),new i("tır",-1,-1)],qr=[new i("casına",-1,-1),new i("cesine",-1,-1)],Cr=[new i("di",-1,-1),new i("ti",-1,-1),new i("dik",-1,-1),new i("tik",-1,-1),new i("duk",-1,-1),new i("tuk",-1,-1),new i("dük",-1,-1),new i("tük",-1,-1),new i("dık",-1,-1),new i("tık",-1,-1),new i("dim",-1,-1),new i("tim",-1,-1),new i("dum",-1,-1),new i("tum",-1,-1),new i("düm",-1,-1),new i("tüm",-1,-1),new i("dım",-1,-1),new i("tım",-1,-1),new i("din",-1,-1),new i("tin",-1,-1),new i("dun",-1,-1),new i("tun",-1,-1),new i("dün",-1,-1),new i("tün",-1,-1),new i("dın",-1,-1),new i("tın",-1,-1),new i("du",-1,-1),new i("tu",-1,-1),new i("dü",-1,-1),new i("tü",-1,-1),new i("dı",-1,-1),new i("tı",-1,-1)],Pr=[new i("sa",-1,-1),new i("se",-1,-1),new i("sak",-1,-1),new i("sek",-1,-1),new i("sam",-1,-1),new i("sem",-1,-1),new i("san",-1,-1),new i("sen",-1,-1)],Fr=[new i("miş",-1,-1),new i("muş",-1,-1),new i("müş",-1,-1),new i("mış",-1,-1)],Sr=[new i("b",-1,1),new i("c",-1,2),new i("d",-1,3),new i("ğ",-1,4)],Wr=[17,65,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,32,8,0,0,0,0,0,0,1],Lr=[1,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,1],xr=[1,64,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],Ar=[17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,130],Er=[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],jr=[17],Tr=[65],Zr=[65],Br=[["a",xr,97,305],["e",Ar,101,252],["ı",Er,97,305],["i",jr,101,105],["o",Tr,111,117],["ö",Zr,246,252],["u",Tr,111,117]],Dr=new e;this.setCurrent=function(r){Dr.setCurrent(r)},this.getCurrent=function(){return Dr.getCurrent()},this.stem=function(){return!!($()&&(Dr.limit_backward=Dr.cursor,Dr.cursor=Dr.limit,J(),Dr.cursor=Dr.limit,nr&&(R(),Dr.cursor=Dr.limit_backward,er())))}};return function(r){return"function"==typeof r.update?r.update(function(r){return n.setCurrent(r),n.stem(),n.getCurrent()}):(n.setCurrent(r),n.stem(),n.getCurrent())}}(),r.Pipeline.registerFunction(r.tr.stemmer,"stemmer-tr"),r.tr.stopWordFilter=r.generateStopWordFilter("acaba altmış altı ama ancak arada aslında ayrıca bana bazı belki ben benden beni benim beri beş bile bin bir biri birkaç birkez birçok birşey birşeyi biz bizden bize bizi bizim bu buna bunda bundan bunlar bunları bunların bunu bunun burada böyle böylece da daha dahi de defa değil diye diğer doksan dokuz dolayı dolayısıyla dört edecek eden ederek edilecek ediliyor edilmesi ediyor elli en etmesi etti ettiği ettiğini eğer gibi göre halen hangi hatta hem henüz hep hepsi her herhangi herkesin hiç hiçbir iki ile ilgili ise itibaren itibariyle için işte kadar karşın katrilyon kendi kendilerine kendini kendisi kendisine kendisini kez ki kim kimden kime kimi kimse kırk milyar milyon mu mü mı nasıl ne neden nedenle nerde nerede nereye niye niçin o olan olarak oldu olduklarını olduğu olduğunu olmadı olmadığı olmak olması olmayan olmaz olsa olsun olup olur olursa oluyor on ona ondan onlar onlardan onları onların onu onun otuz oysa pek rağmen sadece sanki sekiz seksen sen senden seni senin siz sizden sizi sizin tarafından trilyon tüm var vardı ve veya ya yani yapacak yapmak yaptı yaptıkları yaptığı yaptığını yapılan yapılması yapıyor yedi yerine yetmiş yine yirmi yoksa yüz zaten çok çünkü öyle üzere üç şey şeyden şeyi şeyler şu şuna şunda şundan şunları şunu şöyle".split(" ")),r.Pipeline.registerFunction(r.tr.stopWordFilter,"stopWordFilter-tr")}}); \ No newline at end of file diff --git a/v0.13.6/assets/javascripts/lunr/min/lunr.vi.min.js b/v0.13.6/assets/javascripts/lunr/min/lunr.vi.min.js new file mode 100644 index 0000000000..22aed28c49 --- /dev/null +++ b/v0.13.6/assets/javascripts/lunr/min/lunr.vi.min.js @@ -0,0 +1 @@ +!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.vi=function(){this.pipeline.reset(),this.pipeline.add(e.vi.stopWordFilter,e.vi.trimmer)},e.vi.wordCharacters="[A-Za-ẓ̀͐́͑̉̃̓ÂâÊêÔôĂ-ăĐ-đƠ-ơƯ-ư]",e.vi.trimmer=e.trimmerSupport.generateTrimmer(e.vi.wordCharacters),e.Pipeline.registerFunction(e.vi.trimmer,"trimmer-vi"),e.vi.stopWordFilter=e.generateStopWordFilter("là cái nhưng mà".split(" "))}}); \ No newline at end of file diff --git a/v0.13.6/assets/javascripts/lunr/min/lunr.zh.min.js b/v0.13.6/assets/javascripts/lunr/min/lunr.zh.min.js new file mode 100644 index 0000000000..7727bbe24d --- /dev/null +++ b/v0.13.6/assets/javascripts/lunr/min/lunr.zh.min.js @@ -0,0 +1 @@ +!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r(require("nodejieba")):r()(e.lunr)}(this,function(e){return function(r,t){if(void 0===r)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===r.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");var i="2"==r.version[0];r.zh=function(){this.pipeline.reset(),this.pipeline.add(r.zh.trimmer,r.zh.stopWordFilter,r.zh.stemmer),i?this.tokenizer=r.zh.tokenizer:(r.tokenizer&&(r.tokenizer=r.zh.tokenizer),this.tokenizerFn&&(this.tokenizerFn=r.zh.tokenizer))},r.zh.tokenizer=function(n){if(!arguments.length||null==n||void 0==n)return[];if(Array.isArray(n))return n.map(function(e){return i?new r.Token(e.toLowerCase()):e.toLowerCase()});t&&e.load(t);var o=n.toString().trim().toLowerCase(),s=[];e.cut(o,!0).forEach(function(e){s=s.concat(e.split(" "))}),s=s.filter(function(e){return!!e});var u=0;return s.map(function(e,t){if(i){var n=o.indexOf(e,u),s={};return s.position=[n,e.length],s.index=t,u=n,new r.Token(e,s)}return e})},r.zh.wordCharacters="\\w一-龥",r.zh.trimmer=r.trimmerSupport.generateTrimmer(r.zh.wordCharacters),r.Pipeline.registerFunction(r.zh.trimmer,"trimmer-zh"),r.zh.stemmer=function(){return function(e){return e}}(),r.Pipeline.registerFunction(r.zh.stemmer,"stemmer-zh"),r.zh.stopWordFilter=r.generateStopWordFilter("的 一 不 在 人 有 是 为 以 于 上 他 而 后 之 来 及 了 因 下 可 到 由 这 与 也 此 但 并 个 其 已 无 小 我 们 起 最 再 今 去 好 只 又 或 很 亦 某 把 那 你 乃 它 吧 被 比 别 趁 当 从 到 得 打 凡 儿 尔 该 各 给 跟 和 何 还 即 几 既 看 据 距 靠 啦 了 另 么 每 们 嘛 拿 哪 那 您 凭 且 却 让 仍 啥 如 若 使 谁 虽 随 同 所 她 哇 嗡 往 哪 些 向 沿 哟 用 于 咱 则 怎 曾 至 致 着 诸 自".split(" ")),r.Pipeline.registerFunction(r.zh.stopWordFilter,"stopWordFilter-zh")}}); \ No newline at end of file diff --git a/v0.13.6/assets/javascripts/lunr/tinyseg.js b/v0.13.6/assets/javascripts/lunr/tinyseg.js new file mode 100644 index 0000000000..167fa6dd69 --- /dev/null +++ b/v0.13.6/assets/javascripts/lunr/tinyseg.js @@ -0,0 +1,206 @@ +/** + * export the module via AMD, CommonJS or as a browser global + * Export code from https://github.com/umdjs/umd/blob/master/returnExports.js + */ +;(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(factory) + } else if (typeof exports === 'object') { + /** + * Node. Does not work with strict CommonJS, but + * only CommonJS-like environments that support module.exports, + * like Node. + */ + module.exports = factory() + } else { + // Browser globals (root is window) + factory()(root.lunr); + } +}(this, function () { + /** + * Just return a value to define the module export. + * This example returns an object, but the module + * can return a function as the exported value. + */ + + return function(lunr) { + // TinySegmenter 0.1 -- Super compact Japanese tokenizer in Javascript + // (c) 2008 Taku Kudo + // TinySegmenter is freely distributable under the terms of a new BSD licence. + // For details, see http://chasen.org/~taku/software/TinySegmenter/LICENCE.txt + + function TinySegmenter() { + var patterns = { + "[一二三四五六七八九十百千万億兆]":"M", + "[一-龠々〆ヵヶ]":"H", + "[ぁ-ん]":"I", + "[ァ-ヴーア-ン゙ー]":"K", + "[a-zA-Za-zA-Z]":"A", + "[0-90-9]":"N" + } + this.chartype_ = []; + for (var i in patterns) { + var regexp = new RegExp(i); + this.chartype_.push([regexp, patterns[i]]); + } + + this.BIAS__ = -332 + this.BC1__ = {"HH":6,"II":2461,"KH":406,"OH":-1378}; + this.BC2__ = {"AA":-3267,"AI":2744,"AN":-878,"HH":-4070,"HM":-1711,"HN":4012,"HO":3761,"IA":1327,"IH":-1184,"II":-1332,"IK":1721,"IO":5492,"KI":3831,"KK":-8741,"MH":-3132,"MK":3334,"OO":-2920}; + this.BC3__ = {"HH":996,"HI":626,"HK":-721,"HN":-1307,"HO":-836,"IH":-301,"KK":2762,"MK":1079,"MM":4034,"OA":-1652,"OH":266}; + this.BP1__ = {"BB":295,"OB":304,"OO":-125,"UB":352}; + this.BP2__ = {"BO":60,"OO":-1762}; + this.BQ1__ = {"BHH":1150,"BHM":1521,"BII":-1158,"BIM":886,"BMH":1208,"BNH":449,"BOH":-91,"BOO":-2597,"OHI":451,"OIH":-296,"OKA":1851,"OKH":-1020,"OKK":904,"OOO":2965}; + this.BQ2__ = {"BHH":118,"BHI":-1159,"BHM":466,"BIH":-919,"BKK":-1720,"BKO":864,"OHH":-1139,"OHM":-181,"OIH":153,"UHI":-1146}; + this.BQ3__ = {"BHH":-792,"BHI":2664,"BII":-299,"BKI":419,"BMH":937,"BMM":8335,"BNN":998,"BOH":775,"OHH":2174,"OHM":439,"OII":280,"OKH":1798,"OKI":-793,"OKO":-2242,"OMH":-2402,"OOO":11699}; + this.BQ4__ = {"BHH":-3895,"BIH":3761,"BII":-4654,"BIK":1348,"BKK":-1806,"BMI":-3385,"BOO":-12396,"OAH":926,"OHH":266,"OHK":-2036,"ONN":-973}; + this.BW1__ = {",と":660,",同":727,"B1あ":1404,"B1同":542,"、と":660,"、同":727,"」と":1682,"あっ":1505,"いう":1743,"いっ":-2055,"いる":672,"うし":-4817,"うん":665,"から":3472,"がら":600,"こう":-790,"こと":2083,"こん":-1262,"さら":-4143,"さん":4573,"した":2641,"して":1104,"すで":-3399,"そこ":1977,"それ":-871,"たち":1122,"ため":601,"った":3463,"つい":-802,"てい":805,"てき":1249,"でき":1127,"です":3445,"では":844,"とい":-4915,"とみ":1922,"どこ":3887,"ない":5713,"なっ":3015,"など":7379,"なん":-1113,"にし":2468,"には":1498,"にも":1671,"に対":-912,"の一":-501,"の中":741,"ませ":2448,"まで":1711,"まま":2600,"まる":-2155,"やむ":-1947,"よっ":-2565,"れた":2369,"れで":-913,"をし":1860,"を見":731,"亡く":-1886,"京都":2558,"取り":-2784,"大き":-2604,"大阪":1497,"平方":-2314,"引き":-1336,"日本":-195,"本当":-2423,"毎日":-2113,"目指":-724,"B1あ":1404,"B1同":542,"」と":1682}; + this.BW2__ = {"..":-11822,"11":-669,"――":-5730,"−−":-13175,"いう":-1609,"うか":2490,"かし":-1350,"かも":-602,"から":-7194,"かれ":4612,"がい":853,"がら":-3198,"きた":1941,"くな":-1597,"こと":-8392,"この":-4193,"させ":4533,"され":13168,"さん":-3977,"しい":-1819,"しか":-545,"した":5078,"して":972,"しな":939,"その":-3744,"たい":-1253,"たた":-662,"ただ":-3857,"たち":-786,"たと":1224,"たは":-939,"った":4589,"って":1647,"っと":-2094,"てい":6144,"てき":3640,"てく":2551,"ては":-3110,"ても":-3065,"でい":2666,"でき":-1528,"でし":-3828,"です":-4761,"でも":-4203,"とい":1890,"とこ":-1746,"とと":-2279,"との":720,"とみ":5168,"とも":-3941,"ない":-2488,"なが":-1313,"など":-6509,"なの":2614,"なん":3099,"にお":-1615,"にし":2748,"にな":2454,"によ":-7236,"に対":-14943,"に従":-4688,"に関":-11388,"のか":2093,"ので":-7059,"のに":-6041,"のの":-6125,"はい":1073,"はが":-1033,"はず":-2532,"ばれ":1813,"まし":-1316,"まで":-6621,"まれ":5409,"めて":-3153,"もい":2230,"もの":-10713,"らか":-944,"らし":-1611,"らに":-1897,"りし":651,"りま":1620,"れた":4270,"れて":849,"れば":4114,"ろう":6067,"われ":7901,"を通":-11877,"んだ":728,"んな":-4115,"一人":602,"一方":-1375,"一日":970,"一部":-1051,"上が":-4479,"会社":-1116,"出て":2163,"分の":-7758,"同党":970,"同日":-913,"大阪":-2471,"委員":-1250,"少な":-1050,"年度":-8669,"年間":-1626,"府県":-2363,"手権":-1982,"新聞":-4066,"日新":-722,"日本":-7068,"日米":3372,"曜日":-601,"朝鮮":-2355,"本人":-2697,"東京":-1543,"然と":-1384,"社会":-1276,"立て":-990,"第に":-1612,"米国":-4268,"11":-669}; + this.BW3__ = {"あた":-2194,"あり":719,"ある":3846,"い.":-1185,"い。":-1185,"いい":5308,"いえ":2079,"いく":3029,"いた":2056,"いっ":1883,"いる":5600,"いわ":1527,"うち":1117,"うと":4798,"えと":1454,"か.":2857,"か。":2857,"かけ":-743,"かっ":-4098,"かに":-669,"から":6520,"かり":-2670,"が,":1816,"が、":1816,"がき":-4855,"がけ":-1127,"がっ":-913,"がら":-4977,"がり":-2064,"きた":1645,"けど":1374,"こと":7397,"この":1542,"ころ":-2757,"さい":-714,"さを":976,"し,":1557,"し、":1557,"しい":-3714,"した":3562,"して":1449,"しな":2608,"しま":1200,"す.":-1310,"す。":-1310,"する":6521,"ず,":3426,"ず、":3426,"ずに":841,"そう":428,"た.":8875,"た。":8875,"たい":-594,"たの":812,"たり":-1183,"たる":-853,"だ.":4098,"だ。":4098,"だっ":1004,"った":-4748,"って":300,"てい":6240,"てお":855,"ても":302,"です":1437,"でに":-1482,"では":2295,"とう":-1387,"とし":2266,"との":541,"とも":-3543,"どう":4664,"ない":1796,"なく":-903,"など":2135,"に,":-1021,"に、":-1021,"にし":1771,"にな":1906,"には":2644,"の,":-724,"の、":-724,"の子":-1000,"は,":1337,"は、":1337,"べき":2181,"まし":1113,"ます":6943,"まっ":-1549,"まで":6154,"まれ":-793,"らし":1479,"られ":6820,"るる":3818,"れ,":854,"れ、":854,"れた":1850,"れて":1375,"れば":-3246,"れる":1091,"われ":-605,"んだ":606,"んで":798,"カ月":990,"会議":860,"入り":1232,"大会":2217,"始め":1681,"市":965,"新聞":-5055,"日,":974,"日、":974,"社会":2024,"カ月":990}; + this.TC1__ = {"AAA":1093,"HHH":1029,"HHM":580,"HII":998,"HOH":-390,"HOM":-331,"IHI":1169,"IOH":-142,"IOI":-1015,"IOM":467,"MMH":187,"OOI":-1832}; + this.TC2__ = {"HHO":2088,"HII":-1023,"HMM":-1154,"IHI":-1965,"KKH":703,"OII":-2649}; + this.TC3__ = {"AAA":-294,"HHH":346,"HHI":-341,"HII":-1088,"HIK":731,"HOH":-1486,"IHH":128,"IHI":-3041,"IHO":-1935,"IIH":-825,"IIM":-1035,"IOI":-542,"KHH":-1216,"KKA":491,"KKH":-1217,"KOK":-1009,"MHH":-2694,"MHM":-457,"MHO":123,"MMH":-471,"NNH":-1689,"NNO":662,"OHO":-3393}; + this.TC4__ = {"HHH":-203,"HHI":1344,"HHK":365,"HHM":-122,"HHN":182,"HHO":669,"HIH":804,"HII":679,"HOH":446,"IHH":695,"IHO":-2324,"IIH":321,"III":1497,"IIO":656,"IOO":54,"KAK":4845,"KKA":3386,"KKK":3065,"MHH":-405,"MHI":201,"MMH":-241,"MMM":661,"MOM":841}; + this.TQ1__ = {"BHHH":-227,"BHHI":316,"BHIH":-132,"BIHH":60,"BIII":1595,"BNHH":-744,"BOHH":225,"BOOO":-908,"OAKK":482,"OHHH":281,"OHIH":249,"OIHI":200,"OIIH":-68}; + this.TQ2__ = {"BIHH":-1401,"BIII":-1033,"BKAK":-543,"BOOO":-5591}; + this.TQ3__ = {"BHHH":478,"BHHM":-1073,"BHIH":222,"BHII":-504,"BIIH":-116,"BIII":-105,"BMHI":-863,"BMHM":-464,"BOMH":620,"OHHH":346,"OHHI":1729,"OHII":997,"OHMH":481,"OIHH":623,"OIIH":1344,"OKAK":2792,"OKHH":587,"OKKA":679,"OOHH":110,"OOII":-685}; + this.TQ4__ = {"BHHH":-721,"BHHM":-3604,"BHII":-966,"BIIH":-607,"BIII":-2181,"OAAA":-2763,"OAKK":180,"OHHH":-294,"OHHI":2446,"OHHO":480,"OHIH":-1573,"OIHH":1935,"OIHI":-493,"OIIH":626,"OIII":-4007,"OKAK":-8156}; + this.TW1__ = {"につい":-4681,"東京都":2026}; + this.TW2__ = {"ある程":-2049,"いった":-1256,"ころが":-2434,"しょう":3873,"その後":-4430,"だって":-1049,"ていた":1833,"として":-4657,"ともに":-4517,"もので":1882,"一気に":-792,"初めて":-1512,"同時に":-8097,"大きな":-1255,"対して":-2721,"社会党":-3216}; + this.TW3__ = {"いただ":-1734,"してい":1314,"として":-4314,"につい":-5483,"にとっ":-5989,"に当た":-6247,"ので,":-727,"ので、":-727,"のもの":-600,"れから":-3752,"十二月":-2287}; + this.TW4__ = {"いう.":8576,"いう。":8576,"からな":-2348,"してい":2958,"たが,":1516,"たが、":1516,"ている":1538,"という":1349,"ました":5543,"ません":1097,"ようと":-4258,"よると":5865}; + this.UC1__ = {"A":484,"K":93,"M":645,"O":-505}; + this.UC2__ = {"A":819,"H":1059,"I":409,"M":3987,"N":5775,"O":646}; + this.UC3__ = {"A":-1370,"I":2311}; + this.UC4__ = {"A":-2643,"H":1809,"I":-1032,"K":-3450,"M":3565,"N":3876,"O":6646}; + this.UC5__ = {"H":313,"I":-1238,"K":-799,"M":539,"O":-831}; + this.UC6__ = {"H":-506,"I":-253,"K":87,"M":247,"O":-387}; + this.UP1__ = {"O":-214}; + this.UP2__ = {"B":69,"O":935}; + this.UP3__ = {"B":189}; + this.UQ1__ = {"BH":21,"BI":-12,"BK":-99,"BN":142,"BO":-56,"OH":-95,"OI":477,"OK":410,"OO":-2422}; + this.UQ2__ = {"BH":216,"BI":113,"OK":1759}; + this.UQ3__ = {"BA":-479,"BH":42,"BI":1913,"BK":-7198,"BM":3160,"BN":6427,"BO":14761,"OI":-827,"ON":-3212}; + this.UW1__ = {",":156,"、":156,"「":-463,"あ":-941,"う":-127,"が":-553,"き":121,"こ":505,"で":-201,"と":-547,"ど":-123,"に":-789,"の":-185,"は":-847,"も":-466,"や":-470,"よ":182,"ら":-292,"り":208,"れ":169,"を":-446,"ん":-137,"・":-135,"主":-402,"京":-268,"区":-912,"午":871,"国":-460,"大":561,"委":729,"市":-411,"日":-141,"理":361,"生":-408,"県":-386,"都":-718,"「":-463,"・":-135}; + this.UW2__ = {",":-829,"、":-829,"〇":892,"「":-645,"」":3145,"あ":-538,"い":505,"う":134,"お":-502,"か":1454,"が":-856,"く":-412,"こ":1141,"さ":878,"ざ":540,"し":1529,"す":-675,"せ":300,"そ":-1011,"た":188,"だ":1837,"つ":-949,"て":-291,"で":-268,"と":-981,"ど":1273,"な":1063,"に":-1764,"の":130,"は":-409,"ひ":-1273,"べ":1261,"ま":600,"も":-1263,"や":-402,"よ":1639,"り":-579,"る":-694,"れ":571,"を":-2516,"ん":2095,"ア":-587,"カ":306,"キ":568,"ッ":831,"三":-758,"不":-2150,"世":-302,"中":-968,"主":-861,"事":492,"人":-123,"会":978,"保":362,"入":548,"初":-3025,"副":-1566,"北":-3414,"区":-422,"大":-1769,"天":-865,"太":-483,"子":-1519,"学":760,"実":1023,"小":-2009,"市":-813,"年":-1060,"強":1067,"手":-1519,"揺":-1033,"政":1522,"文":-1355,"新":-1682,"日":-1815,"明":-1462,"最":-630,"朝":-1843,"本":-1650,"東":-931,"果":-665,"次":-2378,"民":-180,"気":-1740,"理":752,"発":529,"目":-1584,"相":-242,"県":-1165,"立":-763,"第":810,"米":509,"自":-1353,"行":838,"西":-744,"見":-3874,"調":1010,"議":1198,"込":3041,"開":1758,"間":-1257,"「":-645,"」":3145,"ッ":831,"ア":-587,"カ":306,"キ":568}; + this.UW3__ = {",":4889,"1":-800,"−":-1723,"、":4889,"々":-2311,"〇":5827,"」":2670,"〓":-3573,"あ":-2696,"い":1006,"う":2342,"え":1983,"お":-4864,"か":-1163,"が":3271,"く":1004,"け":388,"げ":401,"こ":-3552,"ご":-3116,"さ":-1058,"し":-395,"す":584,"せ":3685,"そ":-5228,"た":842,"ち":-521,"っ":-1444,"つ":-1081,"て":6167,"で":2318,"と":1691,"ど":-899,"な":-2788,"に":2745,"の":4056,"は":4555,"ひ":-2171,"ふ":-1798,"へ":1199,"ほ":-5516,"ま":-4384,"み":-120,"め":1205,"も":2323,"や":-788,"よ":-202,"ら":727,"り":649,"る":5905,"れ":2773,"わ":-1207,"を":6620,"ん":-518,"ア":551,"グ":1319,"ス":874,"ッ":-1350,"ト":521,"ム":1109,"ル":1591,"ロ":2201,"ン":278,"・":-3794,"一":-1619,"下":-1759,"世":-2087,"両":3815,"中":653,"主":-758,"予":-1193,"二":974,"人":2742,"今":792,"他":1889,"以":-1368,"低":811,"何":4265,"作":-361,"保":-2439,"元":4858,"党":3593,"全":1574,"公":-3030,"六":755,"共":-1880,"円":5807,"再":3095,"分":457,"初":2475,"別":1129,"前":2286,"副":4437,"力":365,"動":-949,"務":-1872,"化":1327,"北":-1038,"区":4646,"千":-2309,"午":-783,"協":-1006,"口":483,"右":1233,"各":3588,"合":-241,"同":3906,"和":-837,"員":4513,"国":642,"型":1389,"場":1219,"外":-241,"妻":2016,"学":-1356,"安":-423,"実":-1008,"家":1078,"小":-513,"少":-3102,"州":1155,"市":3197,"平":-1804,"年":2416,"広":-1030,"府":1605,"度":1452,"建":-2352,"当":-3885,"得":1905,"思":-1291,"性":1822,"戸":-488,"指":-3973,"政":-2013,"教":-1479,"数":3222,"文":-1489,"新":1764,"日":2099,"旧":5792,"昨":-661,"時":-1248,"曜":-951,"最":-937,"月":4125,"期":360,"李":3094,"村":364,"東":-805,"核":5156,"森":2438,"業":484,"氏":2613,"民":-1694,"決":-1073,"法":1868,"海":-495,"無":979,"物":461,"特":-3850,"生":-273,"用":914,"町":1215,"的":7313,"直":-1835,"省":792,"県":6293,"知":-1528,"私":4231,"税":401,"立":-960,"第":1201,"米":7767,"系":3066,"約":3663,"級":1384,"統":-4229,"総":1163,"線":1255,"者":6457,"能":725,"自":-2869,"英":785,"見":1044,"調":-562,"財":-733,"費":1777,"車":1835,"軍":1375,"込":-1504,"通":-1136,"選":-681,"郎":1026,"郡":4404,"部":1200,"金":2163,"長":421,"開":-1432,"間":1302,"関":-1282,"雨":2009,"電":-1045,"非":2066,"駅":1620,"1":-800,"」":2670,"・":-3794,"ッ":-1350,"ア":551,"グ":1319,"ス":874,"ト":521,"ム":1109,"ル":1591,"ロ":2201,"ン":278}; + this.UW4__ = {",":3930,".":3508,"―":-4841,"、":3930,"。":3508,"〇":4999,"「":1895,"」":3798,"〓":-5156,"あ":4752,"い":-3435,"う":-640,"え":-2514,"お":2405,"か":530,"が":6006,"き":-4482,"ぎ":-3821,"く":-3788,"け":-4376,"げ":-4734,"こ":2255,"ご":1979,"さ":2864,"し":-843,"じ":-2506,"す":-731,"ず":1251,"せ":181,"そ":4091,"た":5034,"だ":5408,"ち":-3654,"っ":-5882,"つ":-1659,"て":3994,"で":7410,"と":4547,"な":5433,"に":6499,"ぬ":1853,"ね":1413,"の":7396,"は":8578,"ば":1940,"ひ":4249,"び":-4134,"ふ":1345,"へ":6665,"べ":-744,"ほ":1464,"ま":1051,"み":-2082,"む":-882,"め":-5046,"も":4169,"ゃ":-2666,"や":2795,"ょ":-1544,"よ":3351,"ら":-2922,"り":-9726,"る":-14896,"れ":-2613,"ろ":-4570,"わ":-1783,"を":13150,"ん":-2352,"カ":2145,"コ":1789,"セ":1287,"ッ":-724,"ト":-403,"メ":-1635,"ラ":-881,"リ":-541,"ル":-856,"ン":-3637,"・":-4371,"ー":-11870,"一":-2069,"中":2210,"予":782,"事":-190,"井":-1768,"人":1036,"以":544,"会":950,"体":-1286,"作":530,"側":4292,"先":601,"党":-2006,"共":-1212,"内":584,"円":788,"初":1347,"前":1623,"副":3879,"力":-302,"動":-740,"務":-2715,"化":776,"区":4517,"協":1013,"参":1555,"合":-1834,"和":-681,"員":-910,"器":-851,"回":1500,"国":-619,"園":-1200,"地":866,"場":-1410,"塁":-2094,"士":-1413,"多":1067,"大":571,"子":-4802,"学":-1397,"定":-1057,"寺":-809,"小":1910,"屋":-1328,"山":-1500,"島":-2056,"川":-2667,"市":2771,"年":374,"庁":-4556,"後":456,"性":553,"感":916,"所":-1566,"支":856,"改":787,"政":2182,"教":704,"文":522,"方":-856,"日":1798,"時":1829,"最":845,"月":-9066,"木":-485,"来":-442,"校":-360,"業":-1043,"氏":5388,"民":-2716,"気":-910,"沢":-939,"済":-543,"物":-735,"率":672,"球":-1267,"生":-1286,"産":-1101,"田":-2900,"町":1826,"的":2586,"目":922,"省":-3485,"県":2997,"空":-867,"立":-2112,"第":788,"米":2937,"系":786,"約":2171,"経":1146,"統":-1169,"総":940,"線":-994,"署":749,"者":2145,"能":-730,"般":-852,"行":-792,"規":792,"警":-1184,"議":-244,"谷":-1000,"賞":730,"車":-1481,"軍":1158,"輪":-1433,"込":-3370,"近":929,"道":-1291,"選":2596,"郎":-4866,"都":1192,"野":-1100,"銀":-2213,"長":357,"間":-2344,"院":-2297,"際":-2604,"電":-878,"領":-1659,"題":-792,"館":-1984,"首":1749,"高":2120,"「":1895,"」":3798,"・":-4371,"ッ":-724,"ー":-11870,"カ":2145,"コ":1789,"セ":1287,"ト":-403,"メ":-1635,"ラ":-881,"リ":-541,"ル":-856,"ン":-3637}; + this.UW5__ = {",":465,".":-299,"1":-514,"E2":-32768,"]":-2762,"、":465,"。":-299,"「":363,"あ":1655,"い":331,"う":-503,"え":1199,"お":527,"か":647,"が":-421,"き":1624,"ぎ":1971,"く":312,"げ":-983,"さ":-1537,"し":-1371,"す":-852,"だ":-1186,"ち":1093,"っ":52,"つ":921,"て":-18,"で":-850,"と":-127,"ど":1682,"な":-787,"に":-1224,"の":-635,"は":-578,"べ":1001,"み":502,"め":865,"ゃ":3350,"ょ":854,"り":-208,"る":429,"れ":504,"わ":419,"を":-1264,"ん":327,"イ":241,"ル":451,"ン":-343,"中":-871,"京":722,"会":-1153,"党":-654,"務":3519,"区":-901,"告":848,"員":2104,"大":-1296,"学":-548,"定":1785,"嵐":-1304,"市":-2991,"席":921,"年":1763,"思":872,"所":-814,"挙":1618,"新":-1682,"日":218,"月":-4353,"査":932,"格":1356,"機":-1508,"氏":-1347,"田":240,"町":-3912,"的":-3149,"相":1319,"省":-1052,"県":-4003,"研":-997,"社":-278,"空":-813,"統":1955,"者":-2233,"表":663,"語":-1073,"議":1219,"選":-1018,"郎":-368,"長":786,"間":1191,"題":2368,"館":-689,"1":-514,"E2":-32768,"「":363,"イ":241,"ル":451,"ン":-343}; + this.UW6__ = {",":227,".":808,"1":-270,"E1":306,"、":227,"。":808,"あ":-307,"う":189,"か":241,"が":-73,"く":-121,"こ":-200,"じ":1782,"す":383,"た":-428,"っ":573,"て":-1014,"で":101,"と":-105,"な":-253,"に":-149,"の":-417,"は":-236,"も":-206,"り":187,"る":-135,"を":195,"ル":-673,"ン":-496,"一":-277,"中":201,"件":-800,"会":624,"前":302,"区":1792,"員":-1212,"委":798,"学":-960,"市":887,"広":-695,"後":535,"業":-697,"相":753,"社":-507,"福":974,"空":-822,"者":1811,"連":463,"郎":1082,"1":-270,"E1":306,"ル":-673,"ン":-496}; + + return this; + } + TinySegmenter.prototype.ctype_ = function(str) { + for (var i in this.chartype_) { + if (str.match(this.chartype_[i][0])) { + return this.chartype_[i][1]; + } + } + return "O"; + } + + TinySegmenter.prototype.ts_ = function(v) { + if (v) { return v; } + return 0; + } + + TinySegmenter.prototype.segment = function(input) { + if (input == null || input == undefined || input == "") { + return []; + } + var result = []; + var seg = ["B3","B2","B1"]; + var ctype = ["O","O","O"]; + var o = input.split(""); + for (i = 0; i < o.length; ++i) { + seg.push(o[i]); + ctype.push(this.ctype_(o[i])) + } + seg.push("E1"); + seg.push("E2"); + seg.push("E3"); + ctype.push("O"); + ctype.push("O"); + ctype.push("O"); + var word = seg[3]; + var p1 = "U"; + var p2 = "U"; + var p3 = "U"; + for (var i = 4; i < seg.length - 3; ++i) { + var score = this.BIAS__; + var w1 = seg[i-3]; + var w2 = seg[i-2]; + var w3 = seg[i-1]; + var w4 = seg[i]; + var w5 = seg[i+1]; + var w6 = seg[i+2]; + var c1 = ctype[i-3]; + var c2 = ctype[i-2]; + var c3 = ctype[i-1]; + var c4 = ctype[i]; + var c5 = ctype[i+1]; + var c6 = ctype[i+2]; + score += this.ts_(this.UP1__[p1]); + score += this.ts_(this.UP2__[p2]); + score += this.ts_(this.UP3__[p3]); + score += this.ts_(this.BP1__[p1 + p2]); + score += this.ts_(this.BP2__[p2 + p3]); + score += this.ts_(this.UW1__[w1]); + score += this.ts_(this.UW2__[w2]); + score += this.ts_(this.UW3__[w3]); + score += this.ts_(this.UW4__[w4]); + score += this.ts_(this.UW5__[w5]); + score += this.ts_(this.UW6__[w6]); + score += this.ts_(this.BW1__[w2 + w3]); + score += this.ts_(this.BW2__[w3 + w4]); + score += this.ts_(this.BW3__[w4 + w5]); + score += this.ts_(this.TW1__[w1 + w2 + w3]); + score += this.ts_(this.TW2__[w2 + w3 + w4]); + score += this.ts_(this.TW3__[w3 + w4 + w5]); + score += this.ts_(this.TW4__[w4 + w5 + w6]); + score += this.ts_(this.UC1__[c1]); + score += this.ts_(this.UC2__[c2]); + score += this.ts_(this.UC3__[c3]); + score += this.ts_(this.UC4__[c4]); + score += this.ts_(this.UC5__[c5]); + score += this.ts_(this.UC6__[c6]); + score += this.ts_(this.BC1__[c2 + c3]); + score += this.ts_(this.BC2__[c3 + c4]); + score += this.ts_(this.BC3__[c4 + c5]); + score += this.ts_(this.TC1__[c1 + c2 + c3]); + score += this.ts_(this.TC2__[c2 + c3 + c4]); + score += this.ts_(this.TC3__[c3 + c4 + c5]); + score += this.ts_(this.TC4__[c4 + c5 + c6]); + // score += this.ts_(this.TC5__[c4 + c5 + c6]); + score += this.ts_(this.UQ1__[p1 + c1]); + score += this.ts_(this.UQ2__[p2 + c2]); + score += this.ts_(this.UQ3__[p3 + c3]); + score += this.ts_(this.BQ1__[p2 + c2 + c3]); + score += this.ts_(this.BQ2__[p2 + c3 + c4]); + score += this.ts_(this.BQ3__[p3 + c2 + c3]); + score += this.ts_(this.BQ4__[p3 + c3 + c4]); + score += this.ts_(this.TQ1__[p2 + c1 + c2 + c3]); + score += this.ts_(this.TQ2__[p2 + c2 + c3 + c4]); + score += this.ts_(this.TQ3__[p3 + c1 + c2 + c3]); + score += this.ts_(this.TQ4__[p3 + c2 + c3 + c4]); + var p = "O"; + if (score > 0) { + result.push(word); + word = ""; + p = "B"; + } + p1 = p2; + p2 = p3; + p3 = p; + word += seg[i]; + } + result.push(word); + + return result; + } + + lunr.TinySegmenter = TinySegmenter; + }; + +})); \ No newline at end of file diff --git a/v0.13.6/assets/javascripts/lunr/wordcut.js b/v0.13.6/assets/javascripts/lunr/wordcut.js new file mode 100644 index 0000000000..146f4b44bc --- /dev/null +++ b/v0.13.6/assets/javascripts/lunr/wordcut.js @@ -0,0 +1,6708 @@ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}(g.lunr || (g.lunr = {})).wordcut = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 1; + }) + this.addWords(words, false) + } + if(finalize){ + this.finalizeDict(); + } + }, + + dictSeek: function (l, r, ch, strOffset, pos) { + var ans = null; + while (l <= r) { + var m = Math.floor((l + r) / 2), + dict_item = this.dict[m], + len = dict_item.length; + if (len <= strOffset) { + l = m + 1; + } else { + var ch_ = dict_item[strOffset]; + if (ch_ < ch) { + l = m + 1; + } else if (ch_ > ch) { + r = m - 1; + } else { + ans = m; + if (pos == LEFT) { + r = m - 1; + } else { + l = m + 1; + } + } + } + } + return ans; + }, + + isFinal: function (acceptor) { + return this.dict[acceptor.l].length == acceptor.strOffset; + }, + + createAcceptor: function () { + return { + l: 0, + r: this.dict.length - 1, + strOffset: 0, + isFinal: false, + dict: this, + transit: function (ch) { + return this.dict.transit(this, ch); + }, + isError: false, + tag: "DICT", + w: 1, + type: "DICT" + }; + }, + + transit: function (acceptor, ch) { + var l = this.dictSeek(acceptor.l, + acceptor.r, + ch, + acceptor.strOffset, + LEFT); + if (l !== null) { + var r = this.dictSeek(l, + acceptor.r, + ch, + acceptor.strOffset, + RIGHT); + acceptor.l = l; + acceptor.r = r; + acceptor.strOffset++; + acceptor.isFinal = this.isFinal(acceptor); + } else { + acceptor.isError = true; + } + return acceptor; + }, + + sortuniq: function(a){ + return a.sort().filter(function(item, pos, arr){ + return !pos || item != arr[pos - 1]; + }) + }, + + flatten: function(a){ + //[[1,2],[3]] -> [1,2,3] + return [].concat.apply([], a); + } +}; +module.exports = WordcutDict; + +}).call(this,"/dist/tmp") +},{"glob":16,"path":22}],3:[function(require,module,exports){ +var WordRule = { + createAcceptor: function(tag) { + if (tag["WORD_RULE"]) + return null; + + return {strOffset: 0, + isFinal: false, + transit: function(ch) { + var lch = ch.toLowerCase(); + if (lch >= "a" && lch <= "z") { + this.isFinal = true; + this.strOffset++; + } else { + this.isError = true; + } + return this; + }, + isError: false, + tag: "WORD_RULE", + type: "WORD_RULE", + w: 1}; + } +}; + +var NumberRule = { + createAcceptor: function(tag) { + if (tag["NUMBER_RULE"]) + return null; + + return {strOffset: 0, + isFinal: false, + transit: function(ch) { + if (ch >= "0" && ch <= "9") { + this.isFinal = true; + this.strOffset++; + } else { + this.isError = true; + } + return this; + }, + isError: false, + tag: "NUMBER_RULE", + type: "NUMBER_RULE", + w: 1}; + } +}; + +var SpaceRule = { + tag: "SPACE_RULE", + createAcceptor: function(tag) { + + if (tag["SPACE_RULE"]) + return null; + + return {strOffset: 0, + isFinal: false, + transit: function(ch) { + if (ch == " " || ch == "\t" || ch == "\r" || ch == "\n" || + ch == "\u00A0" || ch=="\u2003"//nbsp and emsp + ) { + this.isFinal = true; + this.strOffset++; + } else { + this.isError = true; + } + return this; + }, + isError: false, + tag: SpaceRule.tag, + w: 1, + type: "SPACE_RULE"}; + } +} + +var SingleSymbolRule = { + tag: "SINSYM", + createAcceptor: function(tag) { + return {strOffset: 0, + isFinal: false, + transit: function(ch) { + if (this.strOffset == 0 && ch.match(/^[\@\(\)\/\,\-\."`]$/)) { + this.isFinal = true; + this.strOffset++; + } else { + this.isError = true; + } + return this; + }, + isError: false, + tag: "SINSYM", + w: 1, + type: "SINSYM"}; + } +} + + +var LatinRules = [WordRule, SpaceRule, SingleSymbolRule, NumberRule]; + +module.exports = LatinRules; + +},{}],4:[function(require,module,exports){ +var _ = require("underscore") + , WordcutCore = require("./wordcut_core"); +var PathInfoBuilder = { + + /* + buildByPartAcceptors: function(path, acceptors, i) { + var + var genInfos = partAcceptors.reduce(function(genInfos, acceptor) { + + }, []); + + return genInfos; + } + */ + + buildByAcceptors: function(path, finalAcceptors, i) { + var self = this; + var infos = finalAcceptors.map(function(acceptor) { + var p = i - acceptor.strOffset + 1 + , _info = path[p]; + + var info = {p: p, + mw: _info.mw + (acceptor.mw === undefined ? 0 : acceptor.mw), + w: acceptor.w + _info.w, + unk: (acceptor.unk ? acceptor.unk : 0) + _info.unk, + type: acceptor.type}; + + if (acceptor.type == "PART") { + for(var j = p + 1; j <= i; j++) { + path[j].merge = p; + } + info.merge = p; + } + + return info; + }); + return infos.filter(function(info) { return info; }); + }, + + fallback: function(path, leftBoundary, text, i) { + var _info = path[leftBoundary]; + if (text[i].match(/[\u0E48-\u0E4E]/)) { + if (leftBoundary != 0) + leftBoundary = path[leftBoundary].p; + return {p: leftBoundary, + mw: 0, + w: 1 + _info.w, + unk: 1 + _info.unk, + type: "UNK"}; +/* } else if(leftBoundary > 0 && path[leftBoundary].type !== "UNK") { + leftBoundary = path[leftBoundary].p; + return {p: leftBoundary, + w: 1 + _info.w, + unk: 1 + _info.unk, + type: "UNK"}; */ + } else { + return {p: leftBoundary, + mw: _info.mw, + w: 1 + _info.w, + unk: 1 + _info.unk, + type: "UNK"}; + } + }, + + build: function(path, finalAcceptors, i, leftBoundary, text) { + var basicPathInfos = this.buildByAcceptors(path, finalAcceptors, i); + if (basicPathInfos.length > 0) { + return basicPathInfos; + } else { + return [this.fallback(path, leftBoundary, text, i)]; + } + } +}; + +module.exports = function() { + return _.clone(PathInfoBuilder); +} + +},{"./wordcut_core":8,"underscore":25}],5:[function(require,module,exports){ +var _ = require("underscore"); + + +var PathSelector = { + selectPath: function(paths) { + var path = paths.reduce(function(selectedPath, path) { + if (selectedPath == null) { + return path; + } else { + if (path.unk < selectedPath.unk) + return path; + if (path.unk == selectedPath.unk) { + if (path.mw < selectedPath.mw) + return path + if (path.mw == selectedPath.mw) { + if (path.w < selectedPath.w) + return path; + } + } + return selectedPath; + } + }, null); + return path; + }, + + createPath: function() { + return [{p:null, w:0, unk:0, type: "INIT", mw:0}]; + } +}; + +module.exports = function() { + return _.clone(PathSelector); +}; + +},{"underscore":25}],6:[function(require,module,exports){ +function isMatch(pat, offset, ch) { + if (pat.length <= offset) + return false; + var _ch = pat[offset]; + return _ch == ch || + (_ch.match(/[กข]/) && ch.match(/[ก-ฮ]/)) || + (_ch.match(/[มบ]/) && ch.match(/[ก-ฮ]/)) || + (_ch.match(/\u0E49/) && ch.match(/[\u0E48-\u0E4B]/)); +} + +var Rule0 = { + pat: "เหก็ม", + createAcceptor: function(tag) { + return {strOffset: 0, + isFinal: false, + transit: function(ch) { + if (isMatch(Rule0.pat, this.strOffset,ch)) { + this.isFinal = (this.strOffset + 1 == Rule0.pat.length); + this.strOffset++; + } else { + this.isError = true; + } + return this; + }, + isError: false, + tag: "THAI_RULE", + type: "THAI_RULE", + w: 1}; + } +}; + +var PartRule = { + createAcceptor: function(tag) { + return {strOffset: 0, + patterns: [ + "แก", "เก", "ก้", "กก์", "กา", "กี", "กิ", "กืก" + ], + isFinal: false, + transit: function(ch) { + var offset = this.strOffset; + this.patterns = this.patterns.filter(function(pat) { + return isMatch(pat, offset, ch); + }); + + if (this.patterns.length > 0) { + var len = 1 + offset; + this.isFinal = this.patterns.some(function(pat) { + return pat.length == len; + }); + this.strOffset++; + } else { + this.isError = true; + } + return this; + }, + isError: false, + tag: "PART", + type: "PART", + unk: 1, + w: 1}; + } +}; + +var ThaiRules = [Rule0, PartRule]; + +module.exports = ThaiRules; + +},{}],7:[function(require,module,exports){ +var sys = require("sys") + , WordcutDict = require("./dict") + , WordcutCore = require("./wordcut_core") + , PathInfoBuilder = require("./path_info_builder") + , PathSelector = require("./path_selector") + , Acceptors = require("./acceptors") + , latinRules = require("./latin_rules") + , thaiRules = require("./thai_rules") + , _ = require("underscore"); + + +var Wordcut = Object.create(WordcutCore); +Wordcut.defaultPathInfoBuilder = PathInfoBuilder; +Wordcut.defaultPathSelector = PathSelector; +Wordcut.defaultAcceptors = Acceptors; +Wordcut.defaultLatinRules = latinRules; +Wordcut.defaultThaiRules = thaiRules; +Wordcut.defaultDict = WordcutDict; + + +Wordcut.initNoDict = function(dict_path) { + var self = this; + self.pathInfoBuilder = new self.defaultPathInfoBuilder; + self.pathSelector = new self.defaultPathSelector; + self.acceptors = new self.defaultAcceptors; + self.defaultLatinRules.forEach(function(rule) { + self.acceptors.creators.push(rule); + }); + self.defaultThaiRules.forEach(function(rule) { + self.acceptors.creators.push(rule); + }); +}; + +Wordcut.init = function(dict_path, withDefault, additionalWords) { + withDefault = withDefault || false; + this.initNoDict(); + var dict = _.clone(this.defaultDict); + dict.init(dict_path, withDefault, additionalWords); + this.acceptors.creators.push(dict); +}; + +module.exports = Wordcut; + +},{"./acceptors":1,"./dict":2,"./latin_rules":3,"./path_info_builder":4,"./path_selector":5,"./thai_rules":6,"./wordcut_core":8,"sys":28,"underscore":25}],8:[function(require,module,exports){ +var WordcutCore = { + + buildPath: function(text) { + var self = this + , path = self.pathSelector.createPath() + , leftBoundary = 0; + self.acceptors.reset(); + for (var i = 0; i < text.length; i++) { + var ch = text[i]; + self.acceptors.transit(ch); + + var possiblePathInfos = self + .pathInfoBuilder + .build(path, + self.acceptors.getFinalAcceptors(), + i, + leftBoundary, + text); + var selectedPath = self.pathSelector.selectPath(possiblePathInfos) + + path.push(selectedPath); + if (selectedPath.type !== "UNK") { + leftBoundary = i; + } + } + return path; + }, + + pathToRanges: function(path) { + var e = path.length - 1 + , ranges = []; + + while (e > 0) { + var info = path[e] + , s = info.p; + + if (info.merge !== undefined && ranges.length > 0) { + var r = ranges[ranges.length - 1]; + r.s = info.merge; + s = r.s; + } else { + ranges.push({s:s, e:e}); + } + e = s; + } + return ranges.reverse(); + }, + + rangesToText: function(text, ranges, delimiter) { + return ranges.map(function(r) { + return text.substring(r.s, r.e); + }).join(delimiter); + }, + + cut: function(text, delimiter) { + var path = this.buildPath(text) + , ranges = this.pathToRanges(path); + return this + .rangesToText(text, ranges, + (delimiter === undefined ? "|" : delimiter)); + }, + + cutIntoRanges: function(text, noText) { + var path = this.buildPath(text) + , ranges = this.pathToRanges(path); + + if (!noText) { + ranges.forEach(function(r) { + r.text = text.substring(r.s, r.e); + }); + } + return ranges; + }, + + cutIntoArray: function(text) { + var path = this.buildPath(text) + , ranges = this.pathToRanges(path); + + return ranges.map(function(r) { + return text.substring(r.s, r.e) + }); + } +}; + +module.exports = WordcutCore; + +},{}],9:[function(require,module,exports){ +// http://wiki.commonjs.org/wiki/Unit_Testing/1.0 +// +// THIS IS NOT TESTED NOR LIKELY TO WORK OUTSIDE V8! +// +// Originally from narwhal.js (http://narwhaljs.org) +// Copyright (c) 2009 Thomas Robinson <280north.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the 'Software'), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +// when used in node, this will actually load the util module we depend on +// versus loading the builtin util module as happens otherwise +// this is a bug in node module loading as far as I am concerned +var util = require('util/'); + +var pSlice = Array.prototype.slice; +var hasOwn = Object.prototype.hasOwnProperty; + +// 1. The assert module provides functions that throw +// AssertionError's when particular conditions are not met. The +// assert module must conform to the following interface. + +var assert = module.exports = ok; + +// 2. The AssertionError is defined in assert. +// new assert.AssertionError({ message: message, +// actual: actual, +// expected: expected }) + +assert.AssertionError = function AssertionError(options) { + this.name = 'AssertionError'; + this.actual = options.actual; + this.expected = options.expected; + this.operator = options.operator; + if (options.message) { + this.message = options.message; + this.generatedMessage = false; + } else { + this.message = getMessage(this); + this.generatedMessage = true; + } + var stackStartFunction = options.stackStartFunction || fail; + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, stackStartFunction); + } + else { + // non v8 browsers so we can have a stacktrace + var err = new Error(); + if (err.stack) { + var out = err.stack; + + // try to strip useless frames + var fn_name = stackStartFunction.name; + var idx = out.indexOf('\n' + fn_name); + if (idx >= 0) { + // once we have located the function frame + // we need to strip out everything before it (and its line) + var next_line = out.indexOf('\n', idx + 1); + out = out.substring(next_line + 1); + } + + this.stack = out; + } + } +}; + +// assert.AssertionError instanceof Error +util.inherits(assert.AssertionError, Error); + +function replacer(key, value) { + if (util.isUndefined(value)) { + return '' + value; + } + if (util.isNumber(value) && !isFinite(value)) { + return value.toString(); + } + if (util.isFunction(value) || util.isRegExp(value)) { + return value.toString(); + } + return value; +} + +function truncate(s, n) { + if (util.isString(s)) { + return s.length < n ? s : s.slice(0, n); + } else { + return s; + } +} + +function getMessage(self) { + return truncate(JSON.stringify(self.actual, replacer), 128) + ' ' + + self.operator + ' ' + + truncate(JSON.stringify(self.expected, replacer), 128); +} + +// At present only the three keys mentioned above are used and +// understood by the spec. Implementations or sub modules can pass +// other keys to the AssertionError's constructor - they will be +// ignored. + +// 3. All of the following functions must throw an AssertionError +// when a corresponding condition is not met, with a message that +// may be undefined if not provided. All assertion methods provide +// both the actual and expected values to the assertion error for +// display purposes. + +function fail(actual, expected, message, operator, stackStartFunction) { + throw new assert.AssertionError({ + message: message, + actual: actual, + expected: expected, + operator: operator, + stackStartFunction: stackStartFunction + }); +} + +// EXTENSION! allows for well behaved errors defined elsewhere. +assert.fail = fail; + +// 4. Pure assertion tests whether a value is truthy, as determined +// by !!guard. +// assert.ok(guard, message_opt); +// This statement is equivalent to assert.equal(true, !!guard, +// message_opt);. To test strictly for the value true, use +// assert.strictEqual(true, guard, message_opt);. + +function ok(value, message) { + if (!value) fail(value, true, message, '==', assert.ok); +} +assert.ok = ok; + +// 5. The equality assertion tests shallow, coercive equality with +// ==. +// assert.equal(actual, expected, message_opt); + +assert.equal = function equal(actual, expected, message) { + if (actual != expected) fail(actual, expected, message, '==', assert.equal); +}; + +// 6. The non-equality assertion tests for whether two objects are not equal +// with != assert.notEqual(actual, expected, message_opt); + +assert.notEqual = function notEqual(actual, expected, message) { + if (actual == expected) { + fail(actual, expected, message, '!=', assert.notEqual); + } +}; + +// 7. The equivalence assertion tests a deep equality relation. +// assert.deepEqual(actual, expected, message_opt); + +assert.deepEqual = function deepEqual(actual, expected, message) { + if (!_deepEqual(actual, expected)) { + fail(actual, expected, message, 'deepEqual', assert.deepEqual); + } +}; + +function _deepEqual(actual, expected) { + // 7.1. All identical values are equivalent, as determined by ===. + if (actual === expected) { + return true; + + } else if (util.isBuffer(actual) && util.isBuffer(expected)) { + if (actual.length != expected.length) return false; + + for (var i = 0; i < actual.length; i++) { + if (actual[i] !== expected[i]) return false; + } + + return true; + + // 7.2. If the expected value is a Date object, the actual value is + // equivalent if it is also a Date object that refers to the same time. + } else if (util.isDate(actual) && util.isDate(expected)) { + return actual.getTime() === expected.getTime(); + + // 7.3 If the expected value is a RegExp object, the actual value is + // equivalent if it is also a RegExp object with the same source and + // properties (`global`, `multiline`, `lastIndex`, `ignoreCase`). + } else if (util.isRegExp(actual) && util.isRegExp(expected)) { + return actual.source === expected.source && + actual.global === expected.global && + actual.multiline === expected.multiline && + actual.lastIndex === expected.lastIndex && + actual.ignoreCase === expected.ignoreCase; + + // 7.4. Other pairs that do not both pass typeof value == 'object', + // equivalence is determined by ==. + } else if (!util.isObject(actual) && !util.isObject(expected)) { + return actual == expected; + + // 7.5 For all other Object pairs, including Array objects, equivalence is + // determined by having the same number of owned properties (as verified + // with Object.prototype.hasOwnProperty.call), the same set of keys + // (although not necessarily the same order), equivalent values for every + // corresponding key, and an identical 'prototype' property. Note: this + // accounts for both named and indexed properties on Arrays. + } else { + return objEquiv(actual, expected); + } +} + +function isArguments(object) { + return Object.prototype.toString.call(object) == '[object Arguments]'; +} + +function objEquiv(a, b) { + if (util.isNullOrUndefined(a) || util.isNullOrUndefined(b)) + return false; + // an identical 'prototype' property. + if (a.prototype !== b.prototype) return false; + // if one is a primitive, the other must be same + if (util.isPrimitive(a) || util.isPrimitive(b)) { + return a === b; + } + var aIsArgs = isArguments(a), + bIsArgs = isArguments(b); + if ((aIsArgs && !bIsArgs) || (!aIsArgs && bIsArgs)) + return false; + if (aIsArgs) { + a = pSlice.call(a); + b = pSlice.call(b); + return _deepEqual(a, b); + } + var ka = objectKeys(a), + kb = objectKeys(b), + key, i; + // having the same number of owned properties (keys incorporates + // hasOwnProperty) + if (ka.length != kb.length) + return false; + //the same set of keys (although not necessarily the same order), + ka.sort(); + kb.sort(); + //~~~cheap key test + for (i = ka.length - 1; i >= 0; i--) { + if (ka[i] != kb[i]) + return false; + } + //equivalent values for every corresponding key, and + //~~~possibly expensive deep test + for (i = ka.length - 1; i >= 0; i--) { + key = ka[i]; + if (!_deepEqual(a[key], b[key])) return false; + } + return true; +} + +// 8. The non-equivalence assertion tests for any deep inequality. +// assert.notDeepEqual(actual, expected, message_opt); + +assert.notDeepEqual = function notDeepEqual(actual, expected, message) { + if (_deepEqual(actual, expected)) { + fail(actual, expected, message, 'notDeepEqual', assert.notDeepEqual); + } +}; + +// 9. The strict equality assertion tests strict equality, as determined by ===. +// assert.strictEqual(actual, expected, message_opt); + +assert.strictEqual = function strictEqual(actual, expected, message) { + if (actual !== expected) { + fail(actual, expected, message, '===', assert.strictEqual); + } +}; + +// 10. The strict non-equality assertion tests for strict inequality, as +// determined by !==. assert.notStrictEqual(actual, expected, message_opt); + +assert.notStrictEqual = function notStrictEqual(actual, expected, message) { + if (actual === expected) { + fail(actual, expected, message, '!==', assert.notStrictEqual); + } +}; + +function expectedException(actual, expected) { + if (!actual || !expected) { + return false; + } + + if (Object.prototype.toString.call(expected) == '[object RegExp]') { + return expected.test(actual); + } else if (actual instanceof expected) { + return true; + } else if (expected.call({}, actual) === true) { + return true; + } + + return false; +} + +function _throws(shouldThrow, block, expected, message) { + var actual; + + if (util.isString(expected)) { + message = expected; + expected = null; + } + + try { + block(); + } catch (e) { + actual = e; + } + + message = (expected && expected.name ? ' (' + expected.name + ').' : '.') + + (message ? ' ' + message : '.'); + + if (shouldThrow && !actual) { + fail(actual, expected, 'Missing expected exception' + message); + } + + if (!shouldThrow && expectedException(actual, expected)) { + fail(actual, expected, 'Got unwanted exception' + message); + } + + if ((shouldThrow && actual && expected && + !expectedException(actual, expected)) || (!shouldThrow && actual)) { + throw actual; + } +} + +// 11. Expected to throw an error: +// assert.throws(block, Error_opt, message_opt); + +assert.throws = function(block, /*optional*/error, /*optional*/message) { + _throws.apply(this, [true].concat(pSlice.call(arguments))); +}; + +// EXTENSION! This is annoying to write outside this module. +assert.doesNotThrow = function(block, /*optional*/message) { + _throws.apply(this, [false].concat(pSlice.call(arguments))); +}; + +assert.ifError = function(err) { if (err) {throw err;}}; + +var objectKeys = Object.keys || function (obj) { + var keys = []; + for (var key in obj) { + if (hasOwn.call(obj, key)) keys.push(key); + } + return keys; +}; + +},{"util/":28}],10:[function(require,module,exports){ +'use strict'; +module.exports = balanced; +function balanced(a, b, str) { + if (a instanceof RegExp) a = maybeMatch(a, str); + if (b instanceof RegExp) b = maybeMatch(b, str); + + var r = range(a, b, str); + + return r && { + start: r[0], + end: r[1], + pre: str.slice(0, r[0]), + body: str.slice(r[0] + a.length, r[1]), + post: str.slice(r[1] + b.length) + }; +} + +function maybeMatch(reg, str) { + var m = str.match(reg); + return m ? m[0] : null; +} + +balanced.range = range; +function range(a, b, str) { + var begs, beg, left, right, result; + var ai = str.indexOf(a); + var bi = str.indexOf(b, ai + 1); + var i = ai; + + if (ai >= 0 && bi > 0) { + begs = []; + left = str.length; + + while (i >= 0 && !result) { + if (i == ai) { + begs.push(i); + ai = str.indexOf(a, i + 1); + } else if (begs.length == 1) { + result = [ begs.pop(), bi ]; + } else { + beg = begs.pop(); + if (beg < left) { + left = beg; + right = bi; + } + + bi = str.indexOf(b, i + 1); + } + + i = ai < bi && ai >= 0 ? ai : bi; + } + + if (begs.length) { + result = [ left, right ]; + } + } + + return result; +} + +},{}],11:[function(require,module,exports){ +var concatMap = require('concat-map'); +var balanced = require('balanced-match'); + +module.exports = expandTop; + +var escSlash = '\0SLASH'+Math.random()+'\0'; +var escOpen = '\0OPEN'+Math.random()+'\0'; +var escClose = '\0CLOSE'+Math.random()+'\0'; +var escComma = '\0COMMA'+Math.random()+'\0'; +var escPeriod = '\0PERIOD'+Math.random()+'\0'; + +function numeric(str) { + return parseInt(str, 10) == str + ? parseInt(str, 10) + : str.charCodeAt(0); +} + +function escapeBraces(str) { + return str.split('\\\\').join(escSlash) + .split('\\{').join(escOpen) + .split('\\}').join(escClose) + .split('\\,').join(escComma) + .split('\\.').join(escPeriod); +} + +function unescapeBraces(str) { + return str.split(escSlash).join('\\') + .split(escOpen).join('{') + .split(escClose).join('}') + .split(escComma).join(',') + .split(escPeriod).join('.'); +} + + +// Basically just str.split(","), but handling cases +// where we have nested braced sections, which should be +// treated as individual members, like {a,{b,c},d} +function parseCommaParts(str) { + if (!str) + return ['']; + + var parts = []; + var m = balanced('{', '}', str); + + if (!m) + return str.split(','); + + var pre = m.pre; + var body = m.body; + var post = m.post; + var p = pre.split(','); + + p[p.length-1] += '{' + body + '}'; + var postParts = parseCommaParts(post); + if (post.length) { + p[p.length-1] += postParts.shift(); + p.push.apply(p, postParts); + } + + parts.push.apply(parts, p); + + return parts; +} + +function expandTop(str) { + if (!str) + return []; + + // I don't know why Bash 4.3 does this, but it does. + // Anything starting with {} will have the first two bytes preserved + // but *only* at the top level, so {},a}b will not expand to anything, + // but a{},b}c will be expanded to [a}c,abc]. + // One could argue that this is a bug in Bash, but since the goal of + // this module is to match Bash's rules, we escape a leading {} + if (str.substr(0, 2) === '{}') { + str = '\\{\\}' + str.substr(2); + } + + return expand(escapeBraces(str), true).map(unescapeBraces); +} + +function identity(e) { + return e; +} + +function embrace(str) { + return '{' + str + '}'; +} +function isPadded(el) { + return /^-?0\d/.test(el); +} + +function lte(i, y) { + return i <= y; +} +function gte(i, y) { + return i >= y; +} + +function expand(str, isTop) { + var expansions = []; + + var m = balanced('{', '}', str); + if (!m || /\$$/.test(m.pre)) return [str]; + + var isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body); + var isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body); + var isSequence = isNumericSequence || isAlphaSequence; + var isOptions = m.body.indexOf(',') >= 0; + if (!isSequence && !isOptions) { + // {a},b} + if (m.post.match(/,.*\}/)) { + str = m.pre + '{' + m.body + escClose + m.post; + return expand(str); + } + return [str]; + } + + var n; + if (isSequence) { + n = m.body.split(/\.\./); + } else { + n = parseCommaParts(m.body); + if (n.length === 1) { + // x{{a,b}}y ==> x{a}y x{b}y + n = expand(n[0], false).map(embrace); + if (n.length === 1) { + var post = m.post.length + ? expand(m.post, false) + : ['']; + return post.map(function(p) { + return m.pre + n[0] + p; + }); + } + } + } + + // at this point, n is the parts, and we know it's not a comma set + // with a single entry. + + // no need to expand pre, since it is guaranteed to be free of brace-sets + var pre = m.pre; + var post = m.post.length + ? expand(m.post, false) + : ['']; + + var N; + + if (isSequence) { + var x = numeric(n[0]); + var y = numeric(n[1]); + var width = Math.max(n[0].length, n[1].length) + var incr = n.length == 3 + ? Math.abs(numeric(n[2])) + : 1; + var test = lte; + var reverse = y < x; + if (reverse) { + incr *= -1; + test = gte; + } + var pad = n.some(isPadded); + + N = []; + + for (var i = x; test(i, y); i += incr) { + var c; + if (isAlphaSequence) { + c = String.fromCharCode(i); + if (c === '\\') + c = ''; + } else { + c = String(i); + if (pad) { + var need = width - c.length; + if (need > 0) { + var z = new Array(need + 1).join('0'); + if (i < 0) + c = '-' + z + c.slice(1); + else + c = z + c; + } + } + } + N.push(c); + } + } else { + N = concatMap(n, function(el) { return expand(el, false) }); + } + + for (var j = 0; j < N.length; j++) { + for (var k = 0; k < post.length; k++) { + var expansion = pre + N[j] + post[k]; + if (!isTop || isSequence || expansion) + expansions.push(expansion); + } + } + + return expansions; +} + + +},{"balanced-match":10,"concat-map":13}],12:[function(require,module,exports){ + +},{}],13:[function(require,module,exports){ +module.exports = function (xs, fn) { + var res = []; + for (var i = 0; i < xs.length; i++) { + var x = fn(xs[i], i); + if (isArray(x)) res.push.apply(res, x); + else res.push(x); + } + return res; +}; + +var isArray = Array.isArray || function (xs) { + return Object.prototype.toString.call(xs) === '[object Array]'; +}; + +},{}],14:[function(require,module,exports){ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +function EventEmitter() { + this._events = this._events || {}; + this._maxListeners = this._maxListeners || undefined; +} +module.exports = EventEmitter; + +// Backwards-compat with node 0.10.x +EventEmitter.EventEmitter = EventEmitter; + +EventEmitter.prototype._events = undefined; +EventEmitter.prototype._maxListeners = undefined; + +// By default EventEmitters will print a warning if more than 10 listeners are +// added to it. This is a useful default which helps finding memory leaks. +EventEmitter.defaultMaxListeners = 10; + +// Obviously not all Emitters should be limited to 10. This function allows +// that to be increased. Set to zero for unlimited. +EventEmitter.prototype.setMaxListeners = function(n) { + if (!isNumber(n) || n < 0 || isNaN(n)) + throw TypeError('n must be a positive number'); + this._maxListeners = n; + return this; +}; + +EventEmitter.prototype.emit = function(type) { + var er, handler, len, args, i, listeners; + + if (!this._events) + this._events = {}; + + // If there is no 'error' event listener then throw. + if (type === 'error') { + if (!this._events.error || + (isObject(this._events.error) && !this._events.error.length)) { + er = arguments[1]; + if (er instanceof Error) { + throw er; // Unhandled 'error' event + } + throw TypeError('Uncaught, unspecified "error" event.'); + } + } + + handler = this._events[type]; + + if (isUndefined(handler)) + return false; + + if (isFunction(handler)) { + switch (arguments.length) { + // fast cases + case 1: + handler.call(this); + break; + case 2: + handler.call(this, arguments[1]); + break; + case 3: + handler.call(this, arguments[1], arguments[2]); + break; + // slower + default: + len = arguments.length; + args = new Array(len - 1); + for (i = 1; i < len; i++) + args[i - 1] = arguments[i]; + handler.apply(this, args); + } + } else if (isObject(handler)) { + len = arguments.length; + args = new Array(len - 1); + for (i = 1; i < len; i++) + args[i - 1] = arguments[i]; + + listeners = handler.slice(); + len = listeners.length; + for (i = 0; i < len; i++) + listeners[i].apply(this, args); + } + + return true; +}; + +EventEmitter.prototype.addListener = function(type, listener) { + var m; + + if (!isFunction(listener)) + throw TypeError('listener must be a function'); + + if (!this._events) + this._events = {}; + + // To avoid recursion in the case that type === "newListener"! Before + // adding it to the listeners, first emit "newListener". + if (this._events.newListener) + this.emit('newListener', type, + isFunction(listener.listener) ? + listener.listener : listener); + + if (!this._events[type]) + // Optimize the case of one listener. Don't need the extra array object. + this._events[type] = listener; + else if (isObject(this._events[type])) + // If we've already got an array, just append. + this._events[type].push(listener); + else + // Adding the second element, need to change to array. + this._events[type] = [this._events[type], listener]; + + // Check for listener leak + if (isObject(this._events[type]) && !this._events[type].warned) { + var m; + if (!isUndefined(this._maxListeners)) { + m = this._maxListeners; + } else { + m = EventEmitter.defaultMaxListeners; + } + + if (m && m > 0 && this._events[type].length > m) { + this._events[type].warned = true; + console.error('(node) warning: possible EventEmitter memory ' + + 'leak detected. %d listeners added. ' + + 'Use emitter.setMaxListeners() to increase limit.', + this._events[type].length); + if (typeof console.trace === 'function') { + // not supported in IE 10 + console.trace(); + } + } + } + + return this; +}; + +EventEmitter.prototype.on = EventEmitter.prototype.addListener; + +EventEmitter.prototype.once = function(type, listener) { + if (!isFunction(listener)) + throw TypeError('listener must be a function'); + + var fired = false; + + function g() { + this.removeListener(type, g); + + if (!fired) { + fired = true; + listener.apply(this, arguments); + } + } + + g.listener = listener; + this.on(type, g); + + return this; +}; + +// emits a 'removeListener' event iff the listener was removed +EventEmitter.prototype.removeListener = function(type, listener) { + var list, position, length, i; + + if (!isFunction(listener)) + throw TypeError('listener must be a function'); + + if (!this._events || !this._events[type]) + return this; + + list = this._events[type]; + length = list.length; + position = -1; + + if (list === listener || + (isFunction(list.listener) && list.listener === listener)) { + delete this._events[type]; + if (this._events.removeListener) + this.emit('removeListener', type, listener); + + } else if (isObject(list)) { + for (i = length; i-- > 0;) { + if (list[i] === listener || + (list[i].listener && list[i].listener === listener)) { + position = i; + break; + } + } + + if (position < 0) + return this; + + if (list.length === 1) { + list.length = 0; + delete this._events[type]; + } else { + list.splice(position, 1); + } + + if (this._events.removeListener) + this.emit('removeListener', type, listener); + } + + return this; +}; + +EventEmitter.prototype.removeAllListeners = function(type) { + var key, listeners; + + if (!this._events) + return this; + + // not listening for removeListener, no need to emit + if (!this._events.removeListener) { + if (arguments.length === 0) + this._events = {}; + else if (this._events[type]) + delete this._events[type]; + return this; + } + + // emit removeListener for all listeners on all events + if (arguments.length === 0) { + for (key in this._events) { + if (key === 'removeListener') continue; + this.removeAllListeners(key); + } + this.removeAllListeners('removeListener'); + this._events = {}; + return this; + } + + listeners = this._events[type]; + + if (isFunction(listeners)) { + this.removeListener(type, listeners); + } else { + // LIFO order + while (listeners.length) + this.removeListener(type, listeners[listeners.length - 1]); + } + delete this._events[type]; + + return this; +}; + +EventEmitter.prototype.listeners = function(type) { + var ret; + if (!this._events || !this._events[type]) + ret = []; + else if (isFunction(this._events[type])) + ret = [this._events[type]]; + else + ret = this._events[type].slice(); + return ret; +}; + +EventEmitter.listenerCount = function(emitter, type) { + var ret; + if (!emitter._events || !emitter._events[type]) + ret = 0; + else if (isFunction(emitter._events[type])) + ret = 1; + else + ret = emitter._events[type].length; + return ret; +}; + +function isFunction(arg) { + return typeof arg === 'function'; +} + +function isNumber(arg) { + return typeof arg === 'number'; +} + +function isObject(arg) { + return typeof arg === 'object' && arg !== null; +} + +function isUndefined(arg) { + return arg === void 0; +} + +},{}],15:[function(require,module,exports){ +(function (process){ +exports.alphasort = alphasort +exports.alphasorti = alphasorti +exports.setopts = setopts +exports.ownProp = ownProp +exports.makeAbs = makeAbs +exports.finish = finish +exports.mark = mark +exports.isIgnored = isIgnored +exports.childrenIgnored = childrenIgnored + +function ownProp (obj, field) { + return Object.prototype.hasOwnProperty.call(obj, field) +} + +var path = require("path") +var minimatch = require("minimatch") +var isAbsolute = require("path-is-absolute") +var Minimatch = minimatch.Minimatch + +function alphasorti (a, b) { + return a.toLowerCase().localeCompare(b.toLowerCase()) +} + +function alphasort (a, b) { + return a.localeCompare(b) +} + +function setupIgnores (self, options) { + self.ignore = options.ignore || [] + + if (!Array.isArray(self.ignore)) + self.ignore = [self.ignore] + + if (self.ignore.length) { + self.ignore = self.ignore.map(ignoreMap) + } +} + +function ignoreMap (pattern) { + var gmatcher = null + if (pattern.slice(-3) === '/**') { + var gpattern = pattern.replace(/(\/\*\*)+$/, '') + gmatcher = new Minimatch(gpattern) + } + + return { + matcher: new Minimatch(pattern), + gmatcher: gmatcher + } +} + +function setopts (self, pattern, options) { + if (!options) + options = {} + + // base-matching: just use globstar for that. + if (options.matchBase && -1 === pattern.indexOf("/")) { + if (options.noglobstar) { + throw new Error("base matching requires globstar") + } + pattern = "**/" + pattern + } + + self.silent = !!options.silent + self.pattern = pattern + self.strict = options.strict !== false + self.realpath = !!options.realpath + self.realpathCache = options.realpathCache || Object.create(null) + self.follow = !!options.follow + self.dot = !!options.dot + self.mark = !!options.mark + self.nodir = !!options.nodir + if (self.nodir) + self.mark = true + self.sync = !!options.sync + self.nounique = !!options.nounique + self.nonull = !!options.nonull + self.nosort = !!options.nosort + self.nocase = !!options.nocase + self.stat = !!options.stat + self.noprocess = !!options.noprocess + + self.maxLength = options.maxLength || Infinity + self.cache = options.cache || Object.create(null) + self.statCache = options.statCache || Object.create(null) + self.symlinks = options.symlinks || Object.create(null) + + setupIgnores(self, options) + + self.changedCwd = false + var cwd = process.cwd() + if (!ownProp(options, "cwd")) + self.cwd = cwd + else { + self.cwd = options.cwd + self.changedCwd = path.resolve(options.cwd) !== cwd + } + + self.root = options.root || path.resolve(self.cwd, "/") + self.root = path.resolve(self.root) + if (process.platform === "win32") + self.root = self.root.replace(/\\/g, "/") + + self.nomount = !!options.nomount + + // disable comments and negation unless the user explicitly + // passes in false as the option. + options.nonegate = options.nonegate === false ? false : true + options.nocomment = options.nocomment === false ? false : true + deprecationWarning(options) + + self.minimatch = new Minimatch(pattern, options) + self.options = self.minimatch.options +} + +// TODO(isaacs): remove entirely in v6 +// exported to reset in tests +exports.deprecationWarned +function deprecationWarning(options) { + if (!options.nonegate || !options.nocomment) { + if (process.noDeprecation !== true && !exports.deprecationWarned) { + var msg = 'glob WARNING: comments and negation will be disabled in v6' + if (process.throwDeprecation) + throw new Error(msg) + else if (process.traceDeprecation) + console.trace(msg) + else + console.error(msg) + + exports.deprecationWarned = true + } + } +} + +function finish (self) { + var nou = self.nounique + var all = nou ? [] : Object.create(null) + + for (var i = 0, l = self.matches.length; i < l; i ++) { + var matches = self.matches[i] + if (!matches || Object.keys(matches).length === 0) { + if (self.nonull) { + // do like the shell, and spit out the literal glob + var literal = self.minimatch.globSet[i] + if (nou) + all.push(literal) + else + all[literal] = true + } + } else { + // had matches + var m = Object.keys(matches) + if (nou) + all.push.apply(all, m) + else + m.forEach(function (m) { + all[m] = true + }) + } + } + + if (!nou) + all = Object.keys(all) + + if (!self.nosort) + all = all.sort(self.nocase ? alphasorti : alphasort) + + // at *some* point we statted all of these + if (self.mark) { + for (var i = 0; i < all.length; i++) { + all[i] = self._mark(all[i]) + } + if (self.nodir) { + all = all.filter(function (e) { + return !(/\/$/.test(e)) + }) + } + } + + if (self.ignore.length) + all = all.filter(function(m) { + return !isIgnored(self, m) + }) + + self.found = all +} + +function mark (self, p) { + var abs = makeAbs(self, p) + var c = self.cache[abs] + var m = p + if (c) { + var isDir = c === 'DIR' || Array.isArray(c) + var slash = p.slice(-1) === '/' + + if (isDir && !slash) + m += '/' + else if (!isDir && slash) + m = m.slice(0, -1) + + if (m !== p) { + var mabs = makeAbs(self, m) + self.statCache[mabs] = self.statCache[abs] + self.cache[mabs] = self.cache[abs] + } + } + + return m +} + +// lotta situps... +function makeAbs (self, f) { + var abs = f + if (f.charAt(0) === '/') { + abs = path.join(self.root, f) + } else if (isAbsolute(f) || f === '') { + abs = f + } else if (self.changedCwd) { + abs = path.resolve(self.cwd, f) + } else { + abs = path.resolve(f) + } + return abs +} + + +// Return true, if pattern ends with globstar '**', for the accompanying parent directory. +// Ex:- If node_modules/** is the pattern, add 'node_modules' to ignore list along with it's contents +function isIgnored (self, path) { + if (!self.ignore.length) + return false + + return self.ignore.some(function(item) { + return item.matcher.match(path) || !!(item.gmatcher && item.gmatcher.match(path)) + }) +} + +function childrenIgnored (self, path) { + if (!self.ignore.length) + return false + + return self.ignore.some(function(item) { + return !!(item.gmatcher && item.gmatcher.match(path)) + }) +} + +}).call(this,require('_process')) +},{"_process":24,"minimatch":20,"path":22,"path-is-absolute":23}],16:[function(require,module,exports){ +(function (process){ +// Approach: +// +// 1. Get the minimatch set +// 2. For each pattern in the set, PROCESS(pattern, false) +// 3. Store matches per-set, then uniq them +// +// PROCESS(pattern, inGlobStar) +// Get the first [n] items from pattern that are all strings +// Join these together. This is PREFIX. +// If there is no more remaining, then stat(PREFIX) and +// add to matches if it succeeds. END. +// +// If inGlobStar and PREFIX is symlink and points to dir +// set ENTRIES = [] +// else readdir(PREFIX) as ENTRIES +// If fail, END +// +// with ENTRIES +// If pattern[n] is GLOBSTAR +// // handle the case where the globstar match is empty +// // by pruning it out, and testing the resulting pattern +// PROCESS(pattern[0..n] + pattern[n+1 .. $], false) +// // handle other cases. +// for ENTRY in ENTRIES (not dotfiles) +// // attach globstar + tail onto the entry +// // Mark that this entry is a globstar match +// PROCESS(pattern[0..n] + ENTRY + pattern[n .. $], true) +// +// else // not globstar +// for ENTRY in ENTRIES (not dotfiles, unless pattern[n] is dot) +// Test ENTRY against pattern[n] +// If fails, continue +// If passes, PROCESS(pattern[0..n] + item + pattern[n+1 .. $]) +// +// Caveat: +// Cache all stats and readdirs results to minimize syscall. Since all +// we ever care about is existence and directory-ness, we can just keep +// `true` for files, and [children,...] for directories, or `false` for +// things that don't exist. + +module.exports = glob + +var fs = require('fs') +var minimatch = require('minimatch') +var Minimatch = minimatch.Minimatch +var inherits = require('inherits') +var EE = require('events').EventEmitter +var path = require('path') +var assert = require('assert') +var isAbsolute = require('path-is-absolute') +var globSync = require('./sync.js') +var common = require('./common.js') +var alphasort = common.alphasort +var alphasorti = common.alphasorti +var setopts = common.setopts +var ownProp = common.ownProp +var inflight = require('inflight') +var util = require('util') +var childrenIgnored = common.childrenIgnored +var isIgnored = common.isIgnored + +var once = require('once') + +function glob (pattern, options, cb) { + if (typeof options === 'function') cb = options, options = {} + if (!options) options = {} + + if (options.sync) { + if (cb) + throw new TypeError('callback provided to sync glob') + return globSync(pattern, options) + } + + return new Glob(pattern, options, cb) +} + +glob.sync = globSync +var GlobSync = glob.GlobSync = globSync.GlobSync + +// old api surface +glob.glob = glob + +glob.hasMagic = function (pattern, options_) { + var options = util._extend({}, options_) + options.noprocess = true + + var g = new Glob(pattern, options) + var set = g.minimatch.set + if (set.length > 1) + return true + + for (var j = 0; j < set[0].length; j++) { + if (typeof set[0][j] !== 'string') + return true + } + + return false +} + +glob.Glob = Glob +inherits(Glob, EE) +function Glob (pattern, options, cb) { + if (typeof options === 'function') { + cb = options + options = null + } + + if (options && options.sync) { + if (cb) + throw new TypeError('callback provided to sync glob') + return new GlobSync(pattern, options) + } + + if (!(this instanceof Glob)) + return new Glob(pattern, options, cb) + + setopts(this, pattern, options) + this._didRealPath = false + + // process each pattern in the minimatch set + var n = this.minimatch.set.length + + // The matches are stored as {: true,...} so that + // duplicates are automagically pruned. + // Later, we do an Object.keys() on these. + // Keep them as a list so we can fill in when nonull is set. + this.matches = new Array(n) + + if (typeof cb === 'function') { + cb = once(cb) + this.on('error', cb) + this.on('end', function (matches) { + cb(null, matches) + }) + } + + var self = this + var n = this.minimatch.set.length + this._processing = 0 + this.matches = new Array(n) + + this._emitQueue = [] + this._processQueue = [] + this.paused = false + + if (this.noprocess) + return this + + if (n === 0) + return done() + + for (var i = 0; i < n; i ++) { + this._process(this.minimatch.set[i], i, false, done) + } + + function done () { + --self._processing + if (self._processing <= 0) + self._finish() + } +} + +Glob.prototype._finish = function () { + assert(this instanceof Glob) + if (this.aborted) + return + + if (this.realpath && !this._didRealpath) + return this._realpath() + + common.finish(this) + this.emit('end', this.found) +} + +Glob.prototype._realpath = function () { + if (this._didRealpath) + return + + this._didRealpath = true + + var n = this.matches.length + if (n === 0) + return this._finish() + + var self = this + for (var i = 0; i < this.matches.length; i++) + this._realpathSet(i, next) + + function next () { + if (--n === 0) + self._finish() + } +} + +Glob.prototype._realpathSet = function (index, cb) { + var matchset = this.matches[index] + if (!matchset) + return cb() + + var found = Object.keys(matchset) + var self = this + var n = found.length + + if (n === 0) + return cb() + + var set = this.matches[index] = Object.create(null) + found.forEach(function (p, i) { + // If there's a problem with the stat, then it means that + // one or more of the links in the realpath couldn't be + // resolved. just return the abs value in that case. + p = self._makeAbs(p) + fs.realpath(p, self.realpathCache, function (er, real) { + if (!er) + set[real] = true + else if (er.syscall === 'stat') + set[p] = true + else + self.emit('error', er) // srsly wtf right here + + if (--n === 0) { + self.matches[index] = set + cb() + } + }) + }) +} + +Glob.prototype._mark = function (p) { + return common.mark(this, p) +} + +Glob.prototype._makeAbs = function (f) { + return common.makeAbs(this, f) +} + +Glob.prototype.abort = function () { + this.aborted = true + this.emit('abort') +} + +Glob.prototype.pause = function () { + if (!this.paused) { + this.paused = true + this.emit('pause') + } +} + +Glob.prototype.resume = function () { + if (this.paused) { + this.emit('resume') + this.paused = false + if (this._emitQueue.length) { + var eq = this._emitQueue.slice(0) + this._emitQueue.length = 0 + for (var i = 0; i < eq.length; i ++) { + var e = eq[i] + this._emitMatch(e[0], e[1]) + } + } + if (this._processQueue.length) { + var pq = this._processQueue.slice(0) + this._processQueue.length = 0 + for (var i = 0; i < pq.length; i ++) { + var p = pq[i] + this._processing-- + this._process(p[0], p[1], p[2], p[3]) + } + } + } +} + +Glob.prototype._process = function (pattern, index, inGlobStar, cb) { + assert(this instanceof Glob) + assert(typeof cb === 'function') + + if (this.aborted) + return + + this._processing++ + if (this.paused) { + this._processQueue.push([pattern, index, inGlobStar, cb]) + return + } + + //console.error('PROCESS %d', this._processing, pattern) + + // Get the first [n] parts of pattern that are all strings. + var n = 0 + while (typeof pattern[n] === 'string') { + n ++ + } + // now n is the index of the first one that is *not* a string. + + // see if there's anything else + var prefix + switch (n) { + // if not, then this is rather simple + case pattern.length: + this._processSimple(pattern.join('/'), index, cb) + return + + case 0: + // pattern *starts* with some non-trivial item. + // going to readdir(cwd), but not include the prefix in matches. + prefix = null + break + + default: + // pattern has some string bits in the front. + // whatever it starts with, whether that's 'absolute' like /foo/bar, + // or 'relative' like '../baz' + prefix = pattern.slice(0, n).join('/') + break + } + + var remain = pattern.slice(n) + + // get the list of entries. + var read + if (prefix === null) + read = '.' + else if (isAbsolute(prefix) || isAbsolute(pattern.join('/'))) { + if (!prefix || !isAbsolute(prefix)) + prefix = '/' + prefix + read = prefix + } else + read = prefix + + var abs = this._makeAbs(read) + + //if ignored, skip _processing + if (childrenIgnored(this, read)) + return cb() + + var isGlobStar = remain[0] === minimatch.GLOBSTAR + if (isGlobStar) + this._processGlobStar(prefix, read, abs, remain, index, inGlobStar, cb) + else + this._processReaddir(prefix, read, abs, remain, index, inGlobStar, cb) +} + +Glob.prototype._processReaddir = function (prefix, read, abs, remain, index, inGlobStar, cb) { + var self = this + this._readdir(abs, inGlobStar, function (er, entries) { + return self._processReaddir2(prefix, read, abs, remain, index, inGlobStar, entries, cb) + }) +} + +Glob.prototype._processReaddir2 = function (prefix, read, abs, remain, index, inGlobStar, entries, cb) { + + // if the abs isn't a dir, then nothing can match! + if (!entries) + return cb() + + // It will only match dot entries if it starts with a dot, or if + // dot is set. Stuff like @(.foo|.bar) isn't allowed. + var pn = remain[0] + var negate = !!this.minimatch.negate + var rawGlob = pn._glob + var dotOk = this.dot || rawGlob.charAt(0) === '.' + + var matchedEntries = [] + for (var i = 0; i < entries.length; i++) { + var e = entries[i] + if (e.charAt(0) !== '.' || dotOk) { + var m + if (negate && !prefix) { + m = !e.match(pn) + } else { + m = e.match(pn) + } + if (m) + matchedEntries.push(e) + } + } + + //console.error('prd2', prefix, entries, remain[0]._glob, matchedEntries) + + var len = matchedEntries.length + // If there are no matched entries, then nothing matches. + if (len === 0) + return cb() + + // if this is the last remaining pattern bit, then no need for + // an additional stat *unless* the user has specified mark or + // stat explicitly. We know they exist, since readdir returned + // them. + + if (remain.length === 1 && !this.mark && !this.stat) { + if (!this.matches[index]) + this.matches[index] = Object.create(null) + + for (var i = 0; i < len; i ++) { + var e = matchedEntries[i] + if (prefix) { + if (prefix !== '/') + e = prefix + '/' + e + else + e = prefix + e + } + + if (e.charAt(0) === '/' && !this.nomount) { + e = path.join(this.root, e) + } + this._emitMatch(index, e) + } + // This was the last one, and no stats were needed + return cb() + } + + // now test all matched entries as stand-ins for that part + // of the pattern. + remain.shift() + for (var i = 0; i < len; i ++) { + var e = matchedEntries[i] + var newPattern + if (prefix) { + if (prefix !== '/') + e = prefix + '/' + e + else + e = prefix + e + } + this._process([e].concat(remain), index, inGlobStar, cb) + } + cb() +} + +Glob.prototype._emitMatch = function (index, e) { + if (this.aborted) + return + + if (this.matches[index][e]) + return + + if (isIgnored(this, e)) + return + + if (this.paused) { + this._emitQueue.push([index, e]) + return + } + + var abs = this._makeAbs(e) + + if (this.nodir) { + var c = this.cache[abs] + if (c === 'DIR' || Array.isArray(c)) + return + } + + if (this.mark) + e = this._mark(e) + + this.matches[index][e] = true + + var st = this.statCache[abs] + if (st) + this.emit('stat', e, st) + + this.emit('match', e) +} + +Glob.prototype._readdirInGlobStar = function (abs, cb) { + if (this.aborted) + return + + // follow all symlinked directories forever + // just proceed as if this is a non-globstar situation + if (this.follow) + return this._readdir(abs, false, cb) + + var lstatkey = 'lstat\0' + abs + var self = this + var lstatcb = inflight(lstatkey, lstatcb_) + + if (lstatcb) + fs.lstat(abs, lstatcb) + + function lstatcb_ (er, lstat) { + if (er) + return cb() + + var isSym = lstat.isSymbolicLink() + self.symlinks[abs] = isSym + + // If it's not a symlink or a dir, then it's definitely a regular file. + // don't bother doing a readdir in that case. + if (!isSym && !lstat.isDirectory()) { + self.cache[abs] = 'FILE' + cb() + } else + self._readdir(abs, false, cb) + } +} + +Glob.prototype._readdir = function (abs, inGlobStar, cb) { + if (this.aborted) + return + + cb = inflight('readdir\0'+abs+'\0'+inGlobStar, cb) + if (!cb) + return + + //console.error('RD %j %j', +inGlobStar, abs) + if (inGlobStar && !ownProp(this.symlinks, abs)) + return this._readdirInGlobStar(abs, cb) + + if (ownProp(this.cache, abs)) { + var c = this.cache[abs] + if (!c || c === 'FILE') + return cb() + + if (Array.isArray(c)) + return cb(null, c) + } + + var self = this + fs.readdir(abs, readdirCb(this, abs, cb)) +} + +function readdirCb (self, abs, cb) { + return function (er, entries) { + if (er) + self._readdirError(abs, er, cb) + else + self._readdirEntries(abs, entries, cb) + } +} + +Glob.prototype._readdirEntries = function (abs, entries, cb) { + if (this.aborted) + return + + // if we haven't asked to stat everything, then just + // assume that everything in there exists, so we can avoid + // having to stat it a second time. + if (!this.mark && !this.stat) { + for (var i = 0; i < entries.length; i ++) { + var e = entries[i] + if (abs === '/') + e = abs + e + else + e = abs + '/' + e + this.cache[e] = true + } + } + + this.cache[abs] = entries + return cb(null, entries) +} + +Glob.prototype._readdirError = function (f, er, cb) { + if (this.aborted) + return + + // handle errors, and cache the information + switch (er.code) { + case 'ENOTSUP': // https://github.com/isaacs/node-glob/issues/205 + case 'ENOTDIR': // totally normal. means it *does* exist. + this.cache[this._makeAbs(f)] = 'FILE' + break + + case 'ENOENT': // not terribly unusual + case 'ELOOP': + case 'ENAMETOOLONG': + case 'UNKNOWN': + this.cache[this._makeAbs(f)] = false + break + + default: // some unusual error. Treat as failure. + this.cache[this._makeAbs(f)] = false + if (this.strict) { + this.emit('error', er) + // If the error is handled, then we abort + // if not, we threw out of here + this.abort() + } + if (!this.silent) + console.error('glob error', er) + break + } + + return cb() +} + +Glob.prototype._processGlobStar = function (prefix, read, abs, remain, index, inGlobStar, cb) { + var self = this + this._readdir(abs, inGlobStar, function (er, entries) { + self._processGlobStar2(prefix, read, abs, remain, index, inGlobStar, entries, cb) + }) +} + + +Glob.prototype._processGlobStar2 = function (prefix, read, abs, remain, index, inGlobStar, entries, cb) { + //console.error('pgs2', prefix, remain[0], entries) + + // no entries means not a dir, so it can never have matches + // foo.txt/** doesn't match foo.txt + if (!entries) + return cb() + + // test without the globstar, and with every child both below + // and replacing the globstar. + var remainWithoutGlobStar = remain.slice(1) + var gspref = prefix ? [ prefix ] : [] + var noGlobStar = gspref.concat(remainWithoutGlobStar) + + // the noGlobStar pattern exits the inGlobStar state + this._process(noGlobStar, index, false, cb) + + var isSym = this.symlinks[abs] + var len = entries.length + + // If it's a symlink, and we're in a globstar, then stop + if (isSym && inGlobStar) + return cb() + + for (var i = 0; i < len; i++) { + var e = entries[i] + if (e.charAt(0) === '.' && !this.dot) + continue + + // these two cases enter the inGlobStar state + var instead = gspref.concat(entries[i], remainWithoutGlobStar) + this._process(instead, index, true, cb) + + var below = gspref.concat(entries[i], remain) + this._process(below, index, true, cb) + } + + cb() +} + +Glob.prototype._processSimple = function (prefix, index, cb) { + // XXX review this. Shouldn't it be doing the mounting etc + // before doing stat? kinda weird? + var self = this + this._stat(prefix, function (er, exists) { + self._processSimple2(prefix, index, er, exists, cb) + }) +} +Glob.prototype._processSimple2 = function (prefix, index, er, exists, cb) { + + //console.error('ps2', prefix, exists) + + if (!this.matches[index]) + this.matches[index] = Object.create(null) + + // If it doesn't exist, then just mark the lack of results + if (!exists) + return cb() + + if (prefix && isAbsolute(prefix) && !this.nomount) { + var trail = /[\/\\]$/.test(prefix) + if (prefix.charAt(0) === '/') { + prefix = path.join(this.root, prefix) + } else { + prefix = path.resolve(this.root, prefix) + if (trail) + prefix += '/' + } + } + + if (process.platform === 'win32') + prefix = prefix.replace(/\\/g, '/') + + // Mark this as a match + this._emitMatch(index, prefix) + cb() +} + +// Returns either 'DIR', 'FILE', or false +Glob.prototype._stat = function (f, cb) { + var abs = this._makeAbs(f) + var needDir = f.slice(-1) === '/' + + if (f.length > this.maxLength) + return cb() + + if (!this.stat && ownProp(this.cache, abs)) { + var c = this.cache[abs] + + if (Array.isArray(c)) + c = 'DIR' + + // It exists, but maybe not how we need it + if (!needDir || c === 'DIR') + return cb(null, c) + + if (needDir && c === 'FILE') + return cb() + + // otherwise we have to stat, because maybe c=true + // if we know it exists, but not what it is. + } + + var exists + var stat = this.statCache[abs] + if (stat !== undefined) { + if (stat === false) + return cb(null, stat) + else { + var type = stat.isDirectory() ? 'DIR' : 'FILE' + if (needDir && type === 'FILE') + return cb() + else + return cb(null, type, stat) + } + } + + var self = this + var statcb = inflight('stat\0' + abs, lstatcb_) + if (statcb) + fs.lstat(abs, statcb) + + function lstatcb_ (er, lstat) { + if (lstat && lstat.isSymbolicLink()) { + // If it's a symlink, then treat it as the target, unless + // the target does not exist, then treat it as a file. + return fs.stat(abs, function (er, stat) { + if (er) + self._stat2(f, abs, null, lstat, cb) + else + self._stat2(f, abs, er, stat, cb) + }) + } else { + self._stat2(f, abs, er, lstat, cb) + } + } +} + +Glob.prototype._stat2 = function (f, abs, er, stat, cb) { + if (er) { + this.statCache[abs] = false + return cb() + } + + var needDir = f.slice(-1) === '/' + this.statCache[abs] = stat + + if (abs.slice(-1) === '/' && !stat.isDirectory()) + return cb(null, false, stat) + + var c = stat.isDirectory() ? 'DIR' : 'FILE' + this.cache[abs] = this.cache[abs] || c + + if (needDir && c !== 'DIR') + return cb() + + return cb(null, c, stat) +} + +}).call(this,require('_process')) +},{"./common.js":15,"./sync.js":17,"_process":24,"assert":9,"events":14,"fs":12,"inflight":18,"inherits":19,"minimatch":20,"once":21,"path":22,"path-is-absolute":23,"util":28}],17:[function(require,module,exports){ +(function (process){ +module.exports = globSync +globSync.GlobSync = GlobSync + +var fs = require('fs') +var minimatch = require('minimatch') +var Minimatch = minimatch.Minimatch +var Glob = require('./glob.js').Glob +var util = require('util') +var path = require('path') +var assert = require('assert') +var isAbsolute = require('path-is-absolute') +var common = require('./common.js') +var alphasort = common.alphasort +var alphasorti = common.alphasorti +var setopts = common.setopts +var ownProp = common.ownProp +var childrenIgnored = common.childrenIgnored + +function globSync (pattern, options) { + if (typeof options === 'function' || arguments.length === 3) + throw new TypeError('callback provided to sync glob\n'+ + 'See: https://github.com/isaacs/node-glob/issues/167') + + return new GlobSync(pattern, options).found +} + +function GlobSync (pattern, options) { + if (!pattern) + throw new Error('must provide pattern') + + if (typeof options === 'function' || arguments.length === 3) + throw new TypeError('callback provided to sync glob\n'+ + 'See: https://github.com/isaacs/node-glob/issues/167') + + if (!(this instanceof GlobSync)) + return new GlobSync(pattern, options) + + setopts(this, pattern, options) + + if (this.noprocess) + return this + + var n = this.minimatch.set.length + this.matches = new Array(n) + for (var i = 0; i < n; i ++) { + this._process(this.minimatch.set[i], i, false) + } + this._finish() +} + +GlobSync.prototype._finish = function () { + assert(this instanceof GlobSync) + if (this.realpath) { + var self = this + this.matches.forEach(function (matchset, index) { + var set = self.matches[index] = Object.create(null) + for (var p in matchset) { + try { + p = self._makeAbs(p) + var real = fs.realpathSync(p, self.realpathCache) + set[real] = true + } catch (er) { + if (er.syscall === 'stat') + set[self._makeAbs(p)] = true + else + throw er + } + } + }) + } + common.finish(this) +} + + +GlobSync.prototype._process = function (pattern, index, inGlobStar) { + assert(this instanceof GlobSync) + + // Get the first [n] parts of pattern that are all strings. + var n = 0 + while (typeof pattern[n] === 'string') { + n ++ + } + // now n is the index of the first one that is *not* a string. + + // See if there's anything else + var prefix + switch (n) { + // if not, then this is rather simple + case pattern.length: + this._processSimple(pattern.join('/'), index) + return + + case 0: + // pattern *starts* with some non-trivial item. + // going to readdir(cwd), but not include the prefix in matches. + prefix = null + break + + default: + // pattern has some string bits in the front. + // whatever it starts with, whether that's 'absolute' like /foo/bar, + // or 'relative' like '../baz' + prefix = pattern.slice(0, n).join('/') + break + } + + var remain = pattern.slice(n) + + // get the list of entries. + var read + if (prefix === null) + read = '.' + else if (isAbsolute(prefix) || isAbsolute(pattern.join('/'))) { + if (!prefix || !isAbsolute(prefix)) + prefix = '/' + prefix + read = prefix + } else + read = prefix + + var abs = this._makeAbs(read) + + //if ignored, skip processing + if (childrenIgnored(this, read)) + return + + var isGlobStar = remain[0] === minimatch.GLOBSTAR + if (isGlobStar) + this._processGlobStar(prefix, read, abs, remain, index, inGlobStar) + else + this._processReaddir(prefix, read, abs, remain, index, inGlobStar) +} + + +GlobSync.prototype._processReaddir = function (prefix, read, abs, remain, index, inGlobStar) { + var entries = this._readdir(abs, inGlobStar) + + // if the abs isn't a dir, then nothing can match! + if (!entries) + return + + // It will only match dot entries if it starts with a dot, or if + // dot is set. Stuff like @(.foo|.bar) isn't allowed. + var pn = remain[0] + var negate = !!this.minimatch.negate + var rawGlob = pn._glob + var dotOk = this.dot || rawGlob.charAt(0) === '.' + + var matchedEntries = [] + for (var i = 0; i < entries.length; i++) { + var e = entries[i] + if (e.charAt(0) !== '.' || dotOk) { + var m + if (negate && !prefix) { + m = !e.match(pn) + } else { + m = e.match(pn) + } + if (m) + matchedEntries.push(e) + } + } + + var len = matchedEntries.length + // If there are no matched entries, then nothing matches. + if (len === 0) + return + + // if this is the last remaining pattern bit, then no need for + // an additional stat *unless* the user has specified mark or + // stat explicitly. We know they exist, since readdir returned + // them. + + if (remain.length === 1 && !this.mark && !this.stat) { + if (!this.matches[index]) + this.matches[index] = Object.create(null) + + for (var i = 0; i < len; i ++) { + var e = matchedEntries[i] + if (prefix) { + if (prefix.slice(-1) !== '/') + e = prefix + '/' + e + else + e = prefix + e + } + + if (e.charAt(0) === '/' && !this.nomount) { + e = path.join(this.root, e) + } + this.matches[index][e] = true + } + // This was the last one, and no stats were needed + return + } + + // now test all matched entries as stand-ins for that part + // of the pattern. + remain.shift() + for (var i = 0; i < len; i ++) { + var e = matchedEntries[i] + var newPattern + if (prefix) + newPattern = [prefix, e] + else + newPattern = [e] + this._process(newPattern.concat(remain), index, inGlobStar) + } +} + + +GlobSync.prototype._emitMatch = function (index, e) { + var abs = this._makeAbs(e) + if (this.mark) + e = this._mark(e) + + if (this.matches[index][e]) + return + + if (this.nodir) { + var c = this.cache[this._makeAbs(e)] + if (c === 'DIR' || Array.isArray(c)) + return + } + + this.matches[index][e] = true + if (this.stat) + this._stat(e) +} + + +GlobSync.prototype._readdirInGlobStar = function (abs) { + // follow all symlinked directories forever + // just proceed as if this is a non-globstar situation + if (this.follow) + return this._readdir(abs, false) + + var entries + var lstat + var stat + try { + lstat = fs.lstatSync(abs) + } catch (er) { + // lstat failed, doesn't exist + return null + } + + var isSym = lstat.isSymbolicLink() + this.symlinks[abs] = isSym + + // If it's not a symlink or a dir, then it's definitely a regular file. + // don't bother doing a readdir in that case. + if (!isSym && !lstat.isDirectory()) + this.cache[abs] = 'FILE' + else + entries = this._readdir(abs, false) + + return entries +} + +GlobSync.prototype._readdir = function (abs, inGlobStar) { + var entries + + if (inGlobStar && !ownProp(this.symlinks, abs)) + return this._readdirInGlobStar(abs) + + if (ownProp(this.cache, abs)) { + var c = this.cache[abs] + if (!c || c === 'FILE') + return null + + if (Array.isArray(c)) + return c + } + + try { + return this._readdirEntries(abs, fs.readdirSync(abs)) + } catch (er) { + this._readdirError(abs, er) + return null + } +} + +GlobSync.prototype._readdirEntries = function (abs, entries) { + // if we haven't asked to stat everything, then just + // assume that everything in there exists, so we can avoid + // having to stat it a second time. + if (!this.mark && !this.stat) { + for (var i = 0; i < entries.length; i ++) { + var e = entries[i] + if (abs === '/') + e = abs + e + else + e = abs + '/' + e + this.cache[e] = true + } + } + + this.cache[abs] = entries + + // mark and cache dir-ness + return entries +} + +GlobSync.prototype._readdirError = function (f, er) { + // handle errors, and cache the information + switch (er.code) { + case 'ENOTSUP': // https://github.com/isaacs/node-glob/issues/205 + case 'ENOTDIR': // totally normal. means it *does* exist. + this.cache[this._makeAbs(f)] = 'FILE' + break + + case 'ENOENT': // not terribly unusual + case 'ELOOP': + case 'ENAMETOOLONG': + case 'UNKNOWN': + this.cache[this._makeAbs(f)] = false + break + + default: // some unusual error. Treat as failure. + this.cache[this._makeAbs(f)] = false + if (this.strict) + throw er + if (!this.silent) + console.error('glob error', er) + break + } +} + +GlobSync.prototype._processGlobStar = function (prefix, read, abs, remain, index, inGlobStar) { + + var entries = this._readdir(abs, inGlobStar) + + // no entries means not a dir, so it can never have matches + // foo.txt/** doesn't match foo.txt + if (!entries) + return + + // test without the globstar, and with every child both below + // and replacing the globstar. + var remainWithoutGlobStar = remain.slice(1) + var gspref = prefix ? [ prefix ] : [] + var noGlobStar = gspref.concat(remainWithoutGlobStar) + + // the noGlobStar pattern exits the inGlobStar state + this._process(noGlobStar, index, false) + + var len = entries.length + var isSym = this.symlinks[abs] + + // If it's a symlink, and we're in a globstar, then stop + if (isSym && inGlobStar) + return + + for (var i = 0; i < len; i++) { + var e = entries[i] + if (e.charAt(0) === '.' && !this.dot) + continue + + // these two cases enter the inGlobStar state + var instead = gspref.concat(entries[i], remainWithoutGlobStar) + this._process(instead, index, true) + + var below = gspref.concat(entries[i], remain) + this._process(below, index, true) + } +} + +GlobSync.prototype._processSimple = function (prefix, index) { + // XXX review this. Shouldn't it be doing the mounting etc + // before doing stat? kinda weird? + var exists = this._stat(prefix) + + if (!this.matches[index]) + this.matches[index] = Object.create(null) + + // If it doesn't exist, then just mark the lack of results + if (!exists) + return + + if (prefix && isAbsolute(prefix) && !this.nomount) { + var trail = /[\/\\]$/.test(prefix) + if (prefix.charAt(0) === '/') { + prefix = path.join(this.root, prefix) + } else { + prefix = path.resolve(this.root, prefix) + if (trail) + prefix += '/' + } + } + + if (process.platform === 'win32') + prefix = prefix.replace(/\\/g, '/') + + // Mark this as a match + this.matches[index][prefix] = true +} + +// Returns either 'DIR', 'FILE', or false +GlobSync.prototype._stat = function (f) { + var abs = this._makeAbs(f) + var needDir = f.slice(-1) === '/' + + if (f.length > this.maxLength) + return false + + if (!this.stat && ownProp(this.cache, abs)) { + var c = this.cache[abs] + + if (Array.isArray(c)) + c = 'DIR' + + // It exists, but maybe not how we need it + if (!needDir || c === 'DIR') + return c + + if (needDir && c === 'FILE') + return false + + // otherwise we have to stat, because maybe c=true + // if we know it exists, but not what it is. + } + + var exists + var stat = this.statCache[abs] + if (!stat) { + var lstat + try { + lstat = fs.lstatSync(abs) + } catch (er) { + return false + } + + if (lstat.isSymbolicLink()) { + try { + stat = fs.statSync(abs) + } catch (er) { + stat = lstat + } + } else { + stat = lstat + } + } + + this.statCache[abs] = stat + + var c = stat.isDirectory() ? 'DIR' : 'FILE' + this.cache[abs] = this.cache[abs] || c + + if (needDir && c !== 'DIR') + return false + + return c +} + +GlobSync.prototype._mark = function (p) { + return common.mark(this, p) +} + +GlobSync.prototype._makeAbs = function (f) { + return common.makeAbs(this, f) +} + +}).call(this,require('_process')) +},{"./common.js":15,"./glob.js":16,"_process":24,"assert":9,"fs":12,"minimatch":20,"path":22,"path-is-absolute":23,"util":28}],18:[function(require,module,exports){ +(function (process){ +var wrappy = require('wrappy') +var reqs = Object.create(null) +var once = require('once') + +module.exports = wrappy(inflight) + +function inflight (key, cb) { + if (reqs[key]) { + reqs[key].push(cb) + return null + } else { + reqs[key] = [cb] + return makeres(key) + } +} + +function makeres (key) { + return once(function RES () { + var cbs = reqs[key] + var len = cbs.length + var args = slice(arguments) + + // XXX It's somewhat ambiguous whether a new callback added in this + // pass should be queued for later execution if something in the + // list of callbacks throws, or if it should just be discarded. + // However, it's such an edge case that it hardly matters, and either + // choice is likely as surprising as the other. + // As it happens, we do go ahead and schedule it for later execution. + try { + for (var i = 0; i < len; i++) { + cbs[i].apply(null, args) + } + } finally { + if (cbs.length > len) { + // added more in the interim. + // de-zalgo, just in case, but don't call again. + cbs.splice(0, len) + process.nextTick(function () { + RES.apply(null, args) + }) + } else { + delete reqs[key] + } + } + }) +} + +function slice (args) { + var length = args.length + var array = [] + + for (var i = 0; i < length; i++) array[i] = args[i] + return array +} + +}).call(this,require('_process')) +},{"_process":24,"once":21,"wrappy":29}],19:[function(require,module,exports){ +if (typeof Object.create === 'function') { + // implementation from standard node.js 'util' module + module.exports = function inherits(ctor, superCtor) { + ctor.super_ = superCtor + ctor.prototype = Object.create(superCtor.prototype, { + constructor: { + value: ctor, + enumerable: false, + writable: true, + configurable: true + } + }); + }; +} else { + // old school shim for old browsers + module.exports = function inherits(ctor, superCtor) { + ctor.super_ = superCtor + var TempCtor = function () {} + TempCtor.prototype = superCtor.prototype + ctor.prototype = new TempCtor() + ctor.prototype.constructor = ctor + } +} + +},{}],20:[function(require,module,exports){ +module.exports = minimatch +minimatch.Minimatch = Minimatch + +var path = { sep: '/' } +try { + path = require('path') +} catch (er) {} + +var GLOBSTAR = minimatch.GLOBSTAR = Minimatch.GLOBSTAR = {} +var expand = require('brace-expansion') + +var plTypes = { + '!': { open: '(?:(?!(?:', close: '))[^/]*?)'}, + '?': { open: '(?:', close: ')?' }, + '+': { open: '(?:', close: ')+' }, + '*': { open: '(?:', close: ')*' }, + '@': { open: '(?:', close: ')' } +} + +// any single thing other than / +// don't need to escape / when using new RegExp() +var qmark = '[^/]' + +// * => any number of characters +var star = qmark + '*?' + +// ** when dots are allowed. Anything goes, except .. and . +// not (^ or / followed by one or two dots followed by $ or /), +// followed by anything, any number of times. +var twoStarDot = '(?:(?!(?:\\\/|^)(?:\\.{1,2})($|\\\/)).)*?' + +// not a ^ or / followed by a dot, +// followed by anything, any number of times. +var twoStarNoDot = '(?:(?!(?:\\\/|^)\\.).)*?' + +// characters that need to be escaped in RegExp. +var reSpecials = charSet('().*{}+?[]^$\\!') + +// "abc" -> { a:true, b:true, c:true } +function charSet (s) { + return s.split('').reduce(function (set, c) { + set[c] = true + return set + }, {}) +} + +// normalizes slashes. +var slashSplit = /\/+/ + +minimatch.filter = filter +function filter (pattern, options) { + options = options || {} + return function (p, i, list) { + return minimatch(p, pattern, options) + } +} + +function ext (a, b) { + a = a || {} + b = b || {} + var t = {} + Object.keys(b).forEach(function (k) { + t[k] = b[k] + }) + Object.keys(a).forEach(function (k) { + t[k] = a[k] + }) + return t +} + +minimatch.defaults = function (def) { + if (!def || !Object.keys(def).length) return minimatch + + var orig = minimatch + + var m = function minimatch (p, pattern, options) { + return orig.minimatch(p, pattern, ext(def, options)) + } + + m.Minimatch = function Minimatch (pattern, options) { + return new orig.Minimatch(pattern, ext(def, options)) + } + + return m +} + +Minimatch.defaults = function (def) { + if (!def || !Object.keys(def).length) return Minimatch + return minimatch.defaults(def).Minimatch +} + +function minimatch (p, pattern, options) { + if (typeof pattern !== 'string') { + throw new TypeError('glob pattern string required') + } + + if (!options) options = {} + + // shortcut: comments match nothing. + if (!options.nocomment && pattern.charAt(0) === '#') { + return false + } + + // "" only matches "" + if (pattern.trim() === '') return p === '' + + return new Minimatch(pattern, options).match(p) +} + +function Minimatch (pattern, options) { + if (!(this instanceof Minimatch)) { + return new Minimatch(pattern, options) + } + + if (typeof pattern !== 'string') { + throw new TypeError('glob pattern string required') + } + + if (!options) options = {} + pattern = pattern.trim() + + // windows support: need to use /, not \ + if (path.sep !== '/') { + pattern = pattern.split(path.sep).join('/') + } + + this.options = options + this.set = [] + this.pattern = pattern + this.regexp = null + this.negate = false + this.comment = false + this.empty = false + + // make the set of regexps etc. + this.make() +} + +Minimatch.prototype.debug = function () {} + +Minimatch.prototype.make = make +function make () { + // don't do it more than once. + if (this._made) return + + var pattern = this.pattern + var options = this.options + + // empty patterns and comments match nothing. + if (!options.nocomment && pattern.charAt(0) === '#') { + this.comment = true + return + } + if (!pattern) { + this.empty = true + return + } + + // step 1: figure out negation, etc. + this.parseNegate() + + // step 2: expand braces + var set = this.globSet = this.braceExpand() + + if (options.debug) this.debug = console.error + + this.debug(this.pattern, set) + + // step 3: now we have a set, so turn each one into a series of path-portion + // matching patterns. + // These will be regexps, except in the case of "**", which is + // set to the GLOBSTAR object for globstar behavior, + // and will not contain any / characters + set = this.globParts = set.map(function (s) { + return s.split(slashSplit) + }) + + this.debug(this.pattern, set) + + // glob --> regexps + set = set.map(function (s, si, set) { + return s.map(this.parse, this) + }, this) + + this.debug(this.pattern, set) + + // filter out everything that didn't compile properly. + set = set.filter(function (s) { + return s.indexOf(false) === -1 + }) + + this.debug(this.pattern, set) + + this.set = set +} + +Minimatch.prototype.parseNegate = parseNegate +function parseNegate () { + var pattern = this.pattern + var negate = false + var options = this.options + var negateOffset = 0 + + if (options.nonegate) return + + for (var i = 0, l = pattern.length + ; i < l && pattern.charAt(i) === '!' + ; i++) { + negate = !negate + negateOffset++ + } + + if (negateOffset) this.pattern = pattern.substr(negateOffset) + this.negate = negate +} + +// Brace expansion: +// a{b,c}d -> abd acd +// a{b,}c -> abc ac +// a{0..3}d -> a0d a1d a2d a3d +// a{b,c{d,e}f}g -> abg acdfg acefg +// a{b,c}d{e,f}g -> abdeg acdeg abdeg abdfg +// +// Invalid sets are not expanded. +// a{2..}b -> a{2..}b +// a{b}c -> a{b}c +minimatch.braceExpand = function (pattern, options) { + return braceExpand(pattern, options) +} + +Minimatch.prototype.braceExpand = braceExpand + +function braceExpand (pattern, options) { + if (!options) { + if (this instanceof Minimatch) { + options = this.options + } else { + options = {} + } + } + + pattern = typeof pattern === 'undefined' + ? this.pattern : pattern + + if (typeof pattern === 'undefined') { + throw new TypeError('undefined pattern') + } + + if (options.nobrace || + !pattern.match(/\{.*\}/)) { + // shortcut. no need to expand. + return [pattern] + } + + return expand(pattern) +} + +// parse a component of the expanded set. +// At this point, no pattern may contain "/" in it +// so we're going to return a 2d array, where each entry is the full +// pattern, split on '/', and then turned into a regular expression. +// A regexp is made at the end which joins each array with an +// escaped /, and another full one which joins each regexp with |. +// +// Following the lead of Bash 4.1, note that "**" only has special meaning +// when it is the *only* thing in a path portion. Otherwise, any series +// of * is equivalent to a single *. Globstar behavior is enabled by +// default, and can be disabled by setting options.noglobstar. +Minimatch.prototype.parse = parse +var SUBPARSE = {} +function parse (pattern, isSub) { + if (pattern.length > 1024 * 64) { + throw new TypeError('pattern is too long') + } + + var options = this.options + + // shortcuts + if (!options.noglobstar && pattern === '**') return GLOBSTAR + if (pattern === '') return '' + + var re = '' + var hasMagic = !!options.nocase + var escaping = false + // ? => one single character + var patternListStack = [] + var negativeLists = [] + var stateChar + var inClass = false + var reClassStart = -1 + var classStart = -1 + // . and .. never match anything that doesn't start with ., + // even when options.dot is set. + var patternStart = pattern.charAt(0) === '.' ? '' // anything + // not (start or / followed by . or .. followed by / or end) + : options.dot ? '(?!(?:^|\\\/)\\.{1,2}(?:$|\\\/))' + : '(?!\\.)' + var self = this + + function clearStateChar () { + if (stateChar) { + // we had some state-tracking character + // that wasn't consumed by this pass. + switch (stateChar) { + case '*': + re += star + hasMagic = true + break + case '?': + re += qmark + hasMagic = true + break + default: + re += '\\' + stateChar + break + } + self.debug('clearStateChar %j %j', stateChar, re) + stateChar = false + } + } + + for (var i = 0, len = pattern.length, c + ; (i < len) && (c = pattern.charAt(i)) + ; i++) { + this.debug('%s\t%s %s %j', pattern, i, re, c) + + // skip over any that are escaped. + if (escaping && reSpecials[c]) { + re += '\\' + c + escaping = false + continue + } + + switch (c) { + case '/': + // completely not allowed, even escaped. + // Should already be path-split by now. + return false + + case '\\': + clearStateChar() + escaping = true + continue + + // the various stateChar values + // for the "extglob" stuff. + case '?': + case '*': + case '+': + case '@': + case '!': + this.debug('%s\t%s %s %j <-- stateChar', pattern, i, re, c) + + // all of those are literals inside a class, except that + // the glob [!a] means [^a] in regexp + if (inClass) { + this.debug(' in class') + if (c === '!' && i === classStart + 1) c = '^' + re += c + continue + } + + // if we already have a stateChar, then it means + // that there was something like ** or +? in there. + // Handle the stateChar, then proceed with this one. + self.debug('call clearStateChar %j', stateChar) + clearStateChar() + stateChar = c + // if extglob is disabled, then +(asdf|foo) isn't a thing. + // just clear the statechar *now*, rather than even diving into + // the patternList stuff. + if (options.noext) clearStateChar() + continue + + case '(': + if (inClass) { + re += '(' + continue + } + + if (!stateChar) { + re += '\\(' + continue + } + + patternListStack.push({ + type: stateChar, + start: i - 1, + reStart: re.length, + open: plTypes[stateChar].open, + close: plTypes[stateChar].close + }) + // negation is (?:(?!js)[^/]*) + re += stateChar === '!' ? '(?:(?!(?:' : '(?:' + this.debug('plType %j %j', stateChar, re) + stateChar = false + continue + + case ')': + if (inClass || !patternListStack.length) { + re += '\\)' + continue + } + + clearStateChar() + hasMagic = true + var pl = patternListStack.pop() + // negation is (?:(?!js)[^/]*) + // The others are (?:) + re += pl.close + if (pl.type === '!') { + negativeLists.push(pl) + } + pl.reEnd = re.length + continue + + case '|': + if (inClass || !patternListStack.length || escaping) { + re += '\\|' + escaping = false + continue + } + + clearStateChar() + re += '|' + continue + + // these are mostly the same in regexp and glob + case '[': + // swallow any state-tracking char before the [ + clearStateChar() + + if (inClass) { + re += '\\' + c + continue + } + + inClass = true + classStart = i + reClassStart = re.length + re += c + continue + + case ']': + // a right bracket shall lose its special + // meaning and represent itself in + // a bracket expression if it occurs + // first in the list. -- POSIX.2 2.8.3.2 + if (i === classStart + 1 || !inClass) { + re += '\\' + c + escaping = false + continue + } + + // handle the case where we left a class open. + // "[z-a]" is valid, equivalent to "\[z-a\]" + if (inClass) { + // split where the last [ was, make sure we don't have + // an invalid re. if so, re-walk the contents of the + // would-be class to re-translate any characters that + // were passed through as-is + // TODO: It would probably be faster to determine this + // without a try/catch and a new RegExp, but it's tricky + // to do safely. For now, this is safe and works. + var cs = pattern.substring(classStart + 1, i) + try { + RegExp('[' + cs + ']') + } catch (er) { + // not a valid class! + var sp = this.parse(cs, SUBPARSE) + re = re.substr(0, reClassStart) + '\\[' + sp[0] + '\\]' + hasMagic = hasMagic || sp[1] + inClass = false + continue + } + } + + // finish up the class. + hasMagic = true + inClass = false + re += c + continue + + default: + // swallow any state char that wasn't consumed + clearStateChar() + + if (escaping) { + // no need + escaping = false + } else if (reSpecials[c] + && !(c === '^' && inClass)) { + re += '\\' + } + + re += c + + } // switch + } // for + + // handle the case where we left a class open. + // "[abc" is valid, equivalent to "\[abc" + if (inClass) { + // split where the last [ was, and escape it + // this is a huge pita. We now have to re-walk + // the contents of the would-be class to re-translate + // any characters that were passed through as-is + cs = pattern.substr(classStart + 1) + sp = this.parse(cs, SUBPARSE) + re = re.substr(0, reClassStart) + '\\[' + sp[0] + hasMagic = hasMagic || sp[1] + } + + // handle the case where we had a +( thing at the *end* + // of the pattern. + // each pattern list stack adds 3 chars, and we need to go through + // and escape any | chars that were passed through as-is for the regexp. + // Go through and escape them, taking care not to double-escape any + // | chars that were already escaped. + for (pl = patternListStack.pop(); pl; pl = patternListStack.pop()) { + var tail = re.slice(pl.reStart + pl.open.length) + this.debug('setting tail', re, pl) + // maybe some even number of \, then maybe 1 \, followed by a | + tail = tail.replace(/((?:\\{2}){0,64})(\\?)\|/g, function (_, $1, $2) { + if (!$2) { + // the | isn't already escaped, so escape it. + $2 = '\\' + } + + // need to escape all those slashes *again*, without escaping the + // one that we need for escaping the | character. As it works out, + // escaping an even number of slashes can be done by simply repeating + // it exactly after itself. That's why this trick works. + // + // I am sorry that you have to see this. + return $1 + $1 + $2 + '|' + }) + + this.debug('tail=%j\n %s', tail, tail, pl, re) + var t = pl.type === '*' ? star + : pl.type === '?' ? qmark + : '\\' + pl.type + + hasMagic = true + re = re.slice(0, pl.reStart) + t + '\\(' + tail + } + + // handle trailing things that only matter at the very end. + clearStateChar() + if (escaping) { + // trailing \\ + re += '\\\\' + } + + // only need to apply the nodot start if the re starts with + // something that could conceivably capture a dot + var addPatternStart = false + switch (re.charAt(0)) { + case '.': + case '[': + case '(': addPatternStart = true + } + + // Hack to work around lack of negative lookbehind in JS + // A pattern like: *.!(x).!(y|z) needs to ensure that a name + // like 'a.xyz.yz' doesn't match. So, the first negative + // lookahead, has to look ALL the way ahead, to the end of + // the pattern. + for (var n = negativeLists.length - 1; n > -1; n--) { + var nl = negativeLists[n] + + var nlBefore = re.slice(0, nl.reStart) + var nlFirst = re.slice(nl.reStart, nl.reEnd - 8) + var nlLast = re.slice(nl.reEnd - 8, nl.reEnd) + var nlAfter = re.slice(nl.reEnd) + + nlLast += nlAfter + + // Handle nested stuff like *(*.js|!(*.json)), where open parens + // mean that we should *not* include the ) in the bit that is considered + // "after" the negated section. + var openParensBefore = nlBefore.split('(').length - 1 + var cleanAfter = nlAfter + for (i = 0; i < openParensBefore; i++) { + cleanAfter = cleanAfter.replace(/\)[+*?]?/, '') + } + nlAfter = cleanAfter + + var dollar = '' + if (nlAfter === '' && isSub !== SUBPARSE) { + dollar = '$' + } + var newRe = nlBefore + nlFirst + nlAfter + dollar + nlLast + re = newRe + } + + // if the re is not "" at this point, then we need to make sure + // it doesn't match against an empty path part. + // Otherwise a/* will match a/, which it should not. + if (re !== '' && hasMagic) { + re = '(?=.)' + re + } + + if (addPatternStart) { + re = patternStart + re + } + + // parsing just a piece of a larger pattern. + if (isSub === SUBPARSE) { + return [re, hasMagic] + } + + // skip the regexp for non-magical patterns + // unescape anything in it, though, so that it'll be + // an exact match against a file etc. + if (!hasMagic) { + return globUnescape(pattern) + } + + var flags = options.nocase ? 'i' : '' + try { + var regExp = new RegExp('^' + re + '$', flags) + } catch (er) { + // If it was an invalid regular expression, then it can't match + // anything. This trick looks for a character after the end of + // the string, which is of course impossible, except in multi-line + // mode, but it's not a /m regex. + return new RegExp('$.') + } + + regExp._glob = pattern + regExp._src = re + + return regExp +} + +minimatch.makeRe = function (pattern, options) { + return new Minimatch(pattern, options || {}).makeRe() +} + +Minimatch.prototype.makeRe = makeRe +function makeRe () { + if (this.regexp || this.regexp === false) return this.regexp + + // at this point, this.set is a 2d array of partial + // pattern strings, or "**". + // + // It's better to use .match(). This function shouldn't + // be used, really, but it's pretty convenient sometimes, + // when you just want to work with a regex. + var set = this.set + + if (!set.length) { + this.regexp = false + return this.regexp + } + var options = this.options + + var twoStar = options.noglobstar ? star + : options.dot ? twoStarDot + : twoStarNoDot + var flags = options.nocase ? 'i' : '' + + var re = set.map(function (pattern) { + return pattern.map(function (p) { + return (p === GLOBSTAR) ? twoStar + : (typeof p === 'string') ? regExpEscape(p) + : p._src + }).join('\\\/') + }).join('|') + + // must match entire pattern + // ending in a * or ** will make it less strict. + re = '^(?:' + re + ')$' + + // can match anything, as long as it's not this. + if (this.negate) re = '^(?!' + re + ').*$' + + try { + this.regexp = new RegExp(re, flags) + } catch (ex) { + this.regexp = false + } + return this.regexp +} + +minimatch.match = function (list, pattern, options) { + options = options || {} + var mm = new Minimatch(pattern, options) + list = list.filter(function (f) { + return mm.match(f) + }) + if (mm.options.nonull && !list.length) { + list.push(pattern) + } + return list +} + +Minimatch.prototype.match = match +function match (f, partial) { + this.debug('match', f, this.pattern) + // short-circuit in the case of busted things. + // comments, etc. + if (this.comment) return false + if (this.empty) return f === '' + + if (f === '/' && partial) return true + + var options = this.options + + // windows: need to use /, not \ + if (path.sep !== '/') { + f = f.split(path.sep).join('/') + } + + // treat the test path as a set of pathparts. + f = f.split(slashSplit) + this.debug(this.pattern, 'split', f) + + // just ONE of the pattern sets in this.set needs to match + // in order for it to be valid. If negating, then just one + // match means that we have failed. + // Either way, return on the first hit. + + var set = this.set + this.debug(this.pattern, 'set', set) + + // Find the basename of the path by looking for the last non-empty segment + var filename + var i + for (i = f.length - 1; i >= 0; i--) { + filename = f[i] + if (filename) break + } + + for (i = 0; i < set.length; i++) { + var pattern = set[i] + var file = f + if (options.matchBase && pattern.length === 1) { + file = [filename] + } + var hit = this.matchOne(file, pattern, partial) + if (hit) { + if (options.flipNegate) return true + return !this.negate + } + } + + // didn't get any hits. this is success if it's a negative + // pattern, failure otherwise. + if (options.flipNegate) return false + return this.negate +} + +// set partial to true to test if, for example, +// "/a/b" matches the start of "/*/b/*/d" +// Partial means, if you run out of file before you run +// out of pattern, then that's fine, as long as all +// the parts match. +Minimatch.prototype.matchOne = function (file, pattern, partial) { + var options = this.options + + this.debug('matchOne', + { 'this': this, file: file, pattern: pattern }) + + this.debug('matchOne', file.length, pattern.length) + + for (var fi = 0, + pi = 0, + fl = file.length, + pl = pattern.length + ; (fi < fl) && (pi < pl) + ; fi++, pi++) { + this.debug('matchOne loop') + var p = pattern[pi] + var f = file[fi] + + this.debug(pattern, p, f) + + // should be impossible. + // some invalid regexp stuff in the set. + if (p === false) return false + + if (p === GLOBSTAR) { + this.debug('GLOBSTAR', [pattern, p, f]) + + // "**" + // a/**/b/**/c would match the following: + // a/b/x/y/z/c + // a/x/y/z/b/c + // a/b/x/b/x/c + // a/b/c + // To do this, take the rest of the pattern after + // the **, and see if it would match the file remainder. + // If so, return success. + // If not, the ** "swallows" a segment, and try again. + // This is recursively awful. + // + // a/**/b/**/c matching a/b/x/y/z/c + // - a matches a + // - doublestar + // - matchOne(b/x/y/z/c, b/**/c) + // - b matches b + // - doublestar + // - matchOne(x/y/z/c, c) -> no + // - matchOne(y/z/c, c) -> no + // - matchOne(z/c, c) -> no + // - matchOne(c, c) yes, hit + var fr = fi + var pr = pi + 1 + if (pr === pl) { + this.debug('** at the end') + // a ** at the end will just swallow the rest. + // We have found a match. + // however, it will not swallow /.x, unless + // options.dot is set. + // . and .. are *never* matched by **, for explosively + // exponential reasons. + for (; fi < fl; fi++) { + if (file[fi] === '.' || file[fi] === '..' || + (!options.dot && file[fi].charAt(0) === '.')) return false + } + return true + } + + // ok, let's see if we can swallow whatever we can. + while (fr < fl) { + var swallowee = file[fr] + + this.debug('\nglobstar while', file, fr, pattern, pr, swallowee) + + // XXX remove this slice. Just pass the start index. + if (this.matchOne(file.slice(fr), pattern.slice(pr), partial)) { + this.debug('globstar found match!', fr, fl, swallowee) + // found a match. + return true + } else { + // can't swallow "." or ".." ever. + // can only swallow ".foo" when explicitly asked. + if (swallowee === '.' || swallowee === '..' || + (!options.dot && swallowee.charAt(0) === '.')) { + this.debug('dot detected!', file, fr, pattern, pr) + break + } + + // ** swallows a segment, and continue. + this.debug('globstar swallow a segment, and continue') + fr++ + } + } + + // no match was found. + // However, in partial mode, we can't say this is necessarily over. + // If there's more *pattern* left, then + if (partial) { + // ran out of file + this.debug('\n>>> no match, partial?', file, fr, pattern, pr) + if (fr === fl) return true + } + return false + } + + // something other than ** + // non-magic patterns just have to match exactly + // patterns with magic have been turned into regexps. + var hit + if (typeof p === 'string') { + if (options.nocase) { + hit = f.toLowerCase() === p.toLowerCase() + } else { + hit = f === p + } + this.debug('string match', p, f, hit) + } else { + hit = f.match(p) + this.debug('pattern match', p, f, hit) + } + + if (!hit) return false + } + + // Note: ending in / means that we'll get a final "" + // at the end of the pattern. This can only match a + // corresponding "" at the end of the file. + // If the file ends in /, then it can only match a + // a pattern that ends in /, unless the pattern just + // doesn't have any more for it. But, a/b/ should *not* + // match "a/b/*", even though "" matches against the + // [^/]*? pattern, except in partial mode, where it might + // simply not be reached yet. + // However, a/b/ should still satisfy a/* + + // now either we fell off the end of the pattern, or we're done. + if (fi === fl && pi === pl) { + // ran out of pattern and filename at the same time. + // an exact hit! + return true + } else if (fi === fl) { + // ran out of file, but still had pattern left. + // this is ok if we're doing the match as part of + // a glob fs traversal. + return partial + } else if (pi === pl) { + // ran out of pattern, still have file left. + // this is only acceptable if we're on the very last + // empty segment of a file with a trailing slash. + // a/* should match a/b/ + var emptyFileEnd = (fi === fl - 1) && (file[fi] === '') + return emptyFileEnd + } + + // should be unreachable. + throw new Error('wtf?') +} + +// replace stuff like \* with * +function globUnescape (s) { + return s.replace(/\\(.)/g, '$1') +} + +function regExpEscape (s) { + return s.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&') +} + +},{"brace-expansion":11,"path":22}],21:[function(require,module,exports){ +var wrappy = require('wrappy') +module.exports = wrappy(once) +module.exports.strict = wrappy(onceStrict) + +once.proto = once(function () { + Object.defineProperty(Function.prototype, 'once', { + value: function () { + return once(this) + }, + configurable: true + }) + + Object.defineProperty(Function.prototype, 'onceStrict', { + value: function () { + return onceStrict(this) + }, + configurable: true + }) +}) + +function once (fn) { + var f = function () { + if (f.called) return f.value + f.called = true + return f.value = fn.apply(this, arguments) + } + f.called = false + return f +} + +function onceStrict (fn) { + var f = function () { + if (f.called) + throw new Error(f.onceError) + f.called = true + return f.value = fn.apply(this, arguments) + } + var name = fn.name || 'Function wrapped with `once`' + f.onceError = name + " shouldn't be called more than once" + f.called = false + return f +} + +},{"wrappy":29}],22:[function(require,module,exports){ +(function (process){ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +// resolves . and .. elements in a path array with directory names there +// must be no slashes, empty elements, or device names (c:\) in the array +// (so also no leading and trailing slashes - it does not distinguish +// relative and absolute paths) +function normalizeArray(parts, allowAboveRoot) { + // if the path tries to go above the root, `up` ends up > 0 + var up = 0; + for (var i = parts.length - 1; i >= 0; i--) { + var last = parts[i]; + if (last === '.') { + parts.splice(i, 1); + } else if (last === '..') { + parts.splice(i, 1); + up++; + } else if (up) { + parts.splice(i, 1); + up--; + } + } + + // if the path is allowed to go above the root, restore leading ..s + if (allowAboveRoot) { + for (; up--; up) { + parts.unshift('..'); + } + } + + return parts; +} + +// Split a filename into [root, dir, basename, ext], unix version +// 'root' is just a slash, or nothing. +var splitPathRe = + /^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/; +var splitPath = function(filename) { + return splitPathRe.exec(filename).slice(1); +}; + +// path.resolve([from ...], to) +// posix version +exports.resolve = function() { + var resolvedPath = '', + resolvedAbsolute = false; + + for (var i = arguments.length - 1; i >= -1 && !resolvedAbsolute; i--) { + var path = (i >= 0) ? arguments[i] : process.cwd(); + + // Skip empty and invalid entries + if (typeof path !== 'string') { + throw new TypeError('Arguments to path.resolve must be strings'); + } else if (!path) { + continue; + } + + resolvedPath = path + '/' + resolvedPath; + resolvedAbsolute = path.charAt(0) === '/'; + } + + // At this point the path should be resolved to a full absolute path, but + // handle relative paths to be safe (might happen when process.cwd() fails) + + // Normalize the path + resolvedPath = normalizeArray(filter(resolvedPath.split('/'), function(p) { + return !!p; + }), !resolvedAbsolute).join('/'); + + return ((resolvedAbsolute ? '/' : '') + resolvedPath) || '.'; +}; + +// path.normalize(path) +// posix version +exports.normalize = function(path) { + var isAbsolute = exports.isAbsolute(path), + trailingSlash = substr(path, -1) === '/'; + + // Normalize the path + path = normalizeArray(filter(path.split('/'), function(p) { + return !!p; + }), !isAbsolute).join('/'); + + if (!path && !isAbsolute) { + path = '.'; + } + if (path && trailingSlash) { + path += '/'; + } + + return (isAbsolute ? '/' : '') + path; +}; + +// posix version +exports.isAbsolute = function(path) { + return path.charAt(0) === '/'; +}; + +// posix version +exports.join = function() { + var paths = Array.prototype.slice.call(arguments, 0); + return exports.normalize(filter(paths, function(p, index) { + if (typeof p !== 'string') { + throw new TypeError('Arguments to path.join must be strings'); + } + return p; + }).join('/')); +}; + + +// path.relative(from, to) +// posix version +exports.relative = function(from, to) { + from = exports.resolve(from).substr(1); + to = exports.resolve(to).substr(1); + + function trim(arr) { + var start = 0; + for (; start < arr.length; start++) { + if (arr[start] !== '') break; + } + + var end = arr.length - 1; + for (; end >= 0; end--) { + if (arr[end] !== '') break; + } + + if (start > end) return []; + return arr.slice(start, end - start + 1); + } + + var fromParts = trim(from.split('/')); + var toParts = trim(to.split('/')); + + var length = Math.min(fromParts.length, toParts.length); + var samePartsLength = length; + for (var i = 0; i < length; i++) { + if (fromParts[i] !== toParts[i]) { + samePartsLength = i; + break; + } + } + + var outputParts = []; + for (var i = samePartsLength; i < fromParts.length; i++) { + outputParts.push('..'); + } + + outputParts = outputParts.concat(toParts.slice(samePartsLength)); + + return outputParts.join('/'); +}; + +exports.sep = '/'; +exports.delimiter = ':'; + +exports.dirname = function(path) { + var result = splitPath(path), + root = result[0], + dir = result[1]; + + if (!root && !dir) { + // No dirname whatsoever + return '.'; + } + + if (dir) { + // It has a dirname, strip trailing slash + dir = dir.substr(0, dir.length - 1); + } + + return root + dir; +}; + + +exports.basename = function(path, ext) { + var f = splitPath(path)[2]; + // TODO: make this comparison case-insensitive on windows? + if (ext && f.substr(-1 * ext.length) === ext) { + f = f.substr(0, f.length - ext.length); + } + return f; +}; + + +exports.extname = function(path) { + return splitPath(path)[3]; +}; + +function filter (xs, f) { + if (xs.filter) return xs.filter(f); + var res = []; + for (var i = 0; i < xs.length; i++) { + if (f(xs[i], i, xs)) res.push(xs[i]); + } + return res; +} + +// String.prototype.substr - negative index don't work in IE8 +var substr = 'ab'.substr(-1) === 'b' + ? function (str, start, len) { return str.substr(start, len) } + : function (str, start, len) { + if (start < 0) start = str.length + start; + return str.substr(start, len); + } +; + +}).call(this,require('_process')) +},{"_process":24}],23:[function(require,module,exports){ +(function (process){ +'use strict'; + +function posix(path) { + return path.charAt(0) === '/'; +} + +function win32(path) { + // https://github.com/nodejs/node/blob/b3fcc245fb25539909ef1d5eaa01dbf92e168633/lib/path.js#L56 + var splitDeviceRe = /^([a-zA-Z]:|[\\\/]{2}[^\\\/]+[\\\/]+[^\\\/]+)?([\\\/])?([\s\S]*?)$/; + var result = splitDeviceRe.exec(path); + var device = result[1] || ''; + var isUnc = Boolean(device && device.charAt(1) !== ':'); + + // UNC paths are always absolute + return Boolean(result[2] || isUnc); +} + +module.exports = process.platform === 'win32' ? win32 : posix; +module.exports.posix = posix; +module.exports.win32 = win32; + +}).call(this,require('_process')) +},{"_process":24}],24:[function(require,module,exports){ +// shim for using process in browser +var process = module.exports = {}; + +// cached from whatever global is present so that test runners that stub it +// don't break things. But we need to wrap it in a try catch in case it is +// wrapped in strict mode code which doesn't define any globals. It's inside a +// function because try/catches deoptimize in certain engines. + +var cachedSetTimeout; +var cachedClearTimeout; + +function defaultSetTimout() { + throw new Error('setTimeout has not been defined'); +} +function defaultClearTimeout () { + throw new Error('clearTimeout has not been defined'); +} +(function () { + try { + if (typeof setTimeout === 'function') { + cachedSetTimeout = setTimeout; + } else { + cachedSetTimeout = defaultSetTimout; + } + } catch (e) { + cachedSetTimeout = defaultSetTimout; + } + try { + if (typeof clearTimeout === 'function') { + cachedClearTimeout = clearTimeout; + } else { + cachedClearTimeout = defaultClearTimeout; + } + } catch (e) { + cachedClearTimeout = defaultClearTimeout; + } +} ()) +function runTimeout(fun) { + if (cachedSetTimeout === setTimeout) { + //normal enviroments in sane situations + return setTimeout(fun, 0); + } + // if setTimeout wasn't available but was latter defined + if ((cachedSetTimeout === defaultSetTimout || !cachedSetTimeout) && setTimeout) { + cachedSetTimeout = setTimeout; + return setTimeout(fun, 0); + } + try { + // when when somebody has screwed with setTimeout but no I.E. maddness + return cachedSetTimeout(fun, 0); + } catch(e){ + try { + // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally + return cachedSetTimeout.call(null, fun, 0); + } catch(e){ + // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error + return cachedSetTimeout.call(this, fun, 0); + } + } + + +} +function runClearTimeout(marker) { + if (cachedClearTimeout === clearTimeout) { + //normal enviroments in sane situations + return clearTimeout(marker); + } + // if clearTimeout wasn't available but was latter defined + if ((cachedClearTimeout === defaultClearTimeout || !cachedClearTimeout) && clearTimeout) { + cachedClearTimeout = clearTimeout; + return clearTimeout(marker); + } + try { + // when when somebody has screwed with setTimeout but no I.E. maddness + return cachedClearTimeout(marker); + } catch (e){ + try { + // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally + return cachedClearTimeout.call(null, marker); + } catch (e){ + // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error. + // Some versions of I.E. have different rules for clearTimeout vs setTimeout + return cachedClearTimeout.call(this, marker); + } + } + + + +} +var queue = []; +var draining = false; +var currentQueue; +var queueIndex = -1; + +function cleanUpNextTick() { + if (!draining || !currentQueue) { + return; + } + draining = false; + if (currentQueue.length) { + queue = currentQueue.concat(queue); + } else { + queueIndex = -1; + } + if (queue.length) { + drainQueue(); + } +} + +function drainQueue() { + if (draining) { + return; + } + var timeout = runTimeout(cleanUpNextTick); + draining = true; + + var len = queue.length; + while(len) { + currentQueue = queue; + queue = []; + while (++queueIndex < len) { + if (currentQueue) { + currentQueue[queueIndex].run(); + } + } + queueIndex = -1; + len = queue.length; + } + currentQueue = null; + draining = false; + runClearTimeout(timeout); +} + +process.nextTick = function (fun) { + var args = new Array(arguments.length - 1); + if (arguments.length > 1) { + for (var i = 1; i < arguments.length; i++) { + args[i - 1] = arguments[i]; + } + } + queue.push(new Item(fun, args)); + if (queue.length === 1 && !draining) { + runTimeout(drainQueue); + } +}; + +// v8 likes predictible objects +function Item(fun, array) { + this.fun = fun; + this.array = array; +} +Item.prototype.run = function () { + this.fun.apply(null, this.array); +}; +process.title = 'browser'; +process.browser = true; +process.env = {}; +process.argv = []; +process.version = ''; // empty string to avoid regexp issues +process.versions = {}; + +function noop() {} + +process.on = noop; +process.addListener = noop; +process.once = noop; +process.off = noop; +process.removeListener = noop; +process.removeAllListeners = noop; +process.emit = noop; +process.prependListener = noop; +process.prependOnceListener = noop; + +process.listeners = function (name) { return [] } + +process.binding = function (name) { + throw new Error('process.binding is not supported'); +}; + +process.cwd = function () { return '/' }; +process.chdir = function (dir) { + throw new Error('process.chdir is not supported'); +}; +process.umask = function() { return 0; }; + +},{}],25:[function(require,module,exports){ +// Underscore.js 1.8.3 +// http://underscorejs.org +// (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +// Underscore may be freely distributed under the MIT license. + +(function() { + + // Baseline setup + // -------------- + + // Establish the root object, `window` in the browser, or `exports` on the server. + var root = this; + + // Save the previous value of the `_` variable. + var previousUnderscore = root._; + + // Save bytes in the minified (but not gzipped) version: + var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; + + // Create quick reference variables for speed access to core prototypes. + var + push = ArrayProto.push, + slice = ArrayProto.slice, + toString = ObjProto.toString, + hasOwnProperty = ObjProto.hasOwnProperty; + + // All **ECMAScript 5** native function implementations that we hope to use + // are declared here. + var + nativeIsArray = Array.isArray, + nativeKeys = Object.keys, + nativeBind = FuncProto.bind, + nativeCreate = Object.create; + + // Naked function reference for surrogate-prototype-swapping. + var Ctor = function(){}; + + // Create a safe reference to the Underscore object for use below. + var _ = function(obj) { + if (obj instanceof _) return obj; + if (!(this instanceof _)) return new _(obj); + this._wrapped = obj; + }; + + // Export the Underscore object for **Node.js**, with + // backwards-compatibility for the old `require()` API. If we're in + // the browser, add `_` as a global object. + if (typeof exports !== 'undefined') { + if (typeof module !== 'undefined' && module.exports) { + exports = module.exports = _; + } + exports._ = _; + } else { + root._ = _; + } + + // Current version. + _.VERSION = '1.8.3'; + + // Internal function that returns an efficient (for current engines) version + // of the passed-in callback, to be repeatedly applied in other Underscore + // functions. + var optimizeCb = function(func, context, argCount) { + if (context === void 0) return func; + switch (argCount == null ? 3 : argCount) { + case 1: return function(value) { + return func.call(context, value); + }; + case 2: return function(value, other) { + return func.call(context, value, other); + }; + case 3: return function(value, index, collection) { + return func.call(context, value, index, collection); + }; + case 4: return function(accumulator, value, index, collection) { + return func.call(context, accumulator, value, index, collection); + }; + } + return function() { + return func.apply(context, arguments); + }; + }; + + // A mostly-internal function to generate callbacks that can be applied + // to each element in a collection, returning the desired result — either + // identity, an arbitrary callback, a property matcher, or a property accessor. + var cb = function(value, context, argCount) { + if (value == null) return _.identity; + if (_.isFunction(value)) return optimizeCb(value, context, argCount); + if (_.isObject(value)) return _.matcher(value); + return _.property(value); + }; + _.iteratee = function(value, context) { + return cb(value, context, Infinity); + }; + + // An internal function for creating assigner functions. + var createAssigner = function(keysFunc, undefinedOnly) { + return function(obj) { + var length = arguments.length; + if (length < 2 || obj == null) return obj; + for (var index = 1; index < length; index++) { + var source = arguments[index], + keys = keysFunc(source), + l = keys.length; + for (var i = 0; i < l; i++) { + var key = keys[i]; + if (!undefinedOnly || obj[key] === void 0) obj[key] = source[key]; + } + } + return obj; + }; + }; + + // An internal function for creating a new object that inherits from another. + var baseCreate = function(prototype) { + if (!_.isObject(prototype)) return {}; + if (nativeCreate) return nativeCreate(prototype); + Ctor.prototype = prototype; + var result = new Ctor; + Ctor.prototype = null; + return result; + }; + + var property = function(key) { + return function(obj) { + return obj == null ? void 0 : obj[key]; + }; + }; + + // Helper for collection methods to determine whether a collection + // should be iterated as an array or as an object + // Related: http://people.mozilla.org/~jorendorff/es6-draft.html#sec-tolength + // Avoids a very nasty iOS 8 JIT bug on ARM-64. #2094 + var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1; + var getLength = property('length'); + var isArrayLike = function(collection) { + var length = getLength(collection); + return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX; + }; + + // Collection Functions + // -------------------- + + // The cornerstone, an `each` implementation, aka `forEach`. + // Handles raw objects in addition to array-likes. Treats all + // sparse array-likes as if they were dense. + _.each = _.forEach = function(obj, iteratee, context) { + iteratee = optimizeCb(iteratee, context); + var i, length; + if (isArrayLike(obj)) { + for (i = 0, length = obj.length; i < length; i++) { + iteratee(obj[i], i, obj); + } + } else { + var keys = _.keys(obj); + for (i = 0, length = keys.length; i < length; i++) { + iteratee(obj[keys[i]], keys[i], obj); + } + } + return obj; + }; + + // Return the results of applying the iteratee to each element. + _.map = _.collect = function(obj, iteratee, context) { + iteratee = cb(iteratee, context); + var keys = !isArrayLike(obj) && _.keys(obj), + length = (keys || obj).length, + results = Array(length); + for (var index = 0; index < length; index++) { + var currentKey = keys ? keys[index] : index; + results[index] = iteratee(obj[currentKey], currentKey, obj); + } + return results; + }; + + // Create a reducing function iterating left or right. + function createReduce(dir) { + // Optimized iterator function as using arguments.length + // in the main function will deoptimize the, see #1991. + function iterator(obj, iteratee, memo, keys, index, length) { + for (; index >= 0 && index < length; index += dir) { + var currentKey = keys ? keys[index] : index; + memo = iteratee(memo, obj[currentKey], currentKey, obj); + } + return memo; + } + + return function(obj, iteratee, memo, context) { + iteratee = optimizeCb(iteratee, context, 4); + var keys = !isArrayLike(obj) && _.keys(obj), + length = (keys || obj).length, + index = dir > 0 ? 0 : length - 1; + // Determine the initial value if none is provided. + if (arguments.length < 3) { + memo = obj[keys ? keys[index] : index]; + index += dir; + } + return iterator(obj, iteratee, memo, keys, index, length); + }; + } + + // **Reduce** builds up a single result from a list of values, aka `inject`, + // or `foldl`. + _.reduce = _.foldl = _.inject = createReduce(1); + + // The right-associative version of reduce, also known as `foldr`. + _.reduceRight = _.foldr = createReduce(-1); + + // Return the first value which passes a truth test. Aliased as `detect`. + _.find = _.detect = function(obj, predicate, context) { + var key; + if (isArrayLike(obj)) { + key = _.findIndex(obj, predicate, context); + } else { + key = _.findKey(obj, predicate, context); + } + if (key !== void 0 && key !== -1) return obj[key]; + }; + + // Return all the elements that pass a truth test. + // Aliased as `select`. + _.filter = _.select = function(obj, predicate, context) { + var results = []; + predicate = cb(predicate, context); + _.each(obj, function(value, index, list) { + if (predicate(value, index, list)) results.push(value); + }); + return results; + }; + + // Return all the elements for which a truth test fails. + _.reject = function(obj, predicate, context) { + return _.filter(obj, _.negate(cb(predicate)), context); + }; + + // Determine whether all of the elements match a truth test. + // Aliased as `all`. + _.every = _.all = function(obj, predicate, context) { + predicate = cb(predicate, context); + var keys = !isArrayLike(obj) && _.keys(obj), + length = (keys || obj).length; + for (var index = 0; index < length; index++) { + var currentKey = keys ? keys[index] : index; + if (!predicate(obj[currentKey], currentKey, obj)) return false; + } + return true; + }; + + // Determine if at least one element in the object matches a truth test. + // Aliased as `any`. + _.some = _.any = function(obj, predicate, context) { + predicate = cb(predicate, context); + var keys = !isArrayLike(obj) && _.keys(obj), + length = (keys || obj).length; + for (var index = 0; index < length; index++) { + var currentKey = keys ? keys[index] : index; + if (predicate(obj[currentKey], currentKey, obj)) return true; + } + return false; + }; + + // Determine if the array or object contains a given item (using `===`). + // Aliased as `includes` and `include`. + _.contains = _.includes = _.include = function(obj, item, fromIndex, guard) { + if (!isArrayLike(obj)) obj = _.values(obj); + if (typeof fromIndex != 'number' || guard) fromIndex = 0; + return _.indexOf(obj, item, fromIndex) >= 0; + }; + + // Invoke a method (with arguments) on every item in a collection. + _.invoke = function(obj, method) { + var args = slice.call(arguments, 2); + var isFunc = _.isFunction(method); + return _.map(obj, function(value) { + var func = isFunc ? method : value[method]; + return func == null ? func : func.apply(value, args); + }); + }; + + // Convenience version of a common use case of `map`: fetching a property. + _.pluck = function(obj, key) { + return _.map(obj, _.property(key)); + }; + + // Convenience version of a common use case of `filter`: selecting only objects + // containing specific `key:value` pairs. + _.where = function(obj, attrs) { + return _.filter(obj, _.matcher(attrs)); + }; + + // Convenience version of a common use case of `find`: getting the first object + // containing specific `key:value` pairs. + _.findWhere = function(obj, attrs) { + return _.find(obj, _.matcher(attrs)); + }; + + // Return the maximum element (or element-based computation). + _.max = function(obj, iteratee, context) { + var result = -Infinity, lastComputed = -Infinity, + value, computed; + if (iteratee == null && obj != null) { + obj = isArrayLike(obj) ? obj : _.values(obj); + for (var i = 0, length = obj.length; i < length; i++) { + value = obj[i]; + if (value > result) { + result = value; + } + } + } else { + iteratee = cb(iteratee, context); + _.each(obj, function(value, index, list) { + computed = iteratee(value, index, list); + if (computed > lastComputed || computed === -Infinity && result === -Infinity) { + result = value; + lastComputed = computed; + } + }); + } + return result; + }; + + // Return the minimum element (or element-based computation). + _.min = function(obj, iteratee, context) { + var result = Infinity, lastComputed = Infinity, + value, computed; + if (iteratee == null && obj != null) { + obj = isArrayLike(obj) ? obj : _.values(obj); + for (var i = 0, length = obj.length; i < length; i++) { + value = obj[i]; + if (value < result) { + result = value; + } + } + } else { + iteratee = cb(iteratee, context); + _.each(obj, function(value, index, list) { + computed = iteratee(value, index, list); + if (computed < lastComputed || computed === Infinity && result === Infinity) { + result = value; + lastComputed = computed; + } + }); + } + return result; + }; + + // Shuffle a collection, using the modern version of the + // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle). + _.shuffle = function(obj) { + var set = isArrayLike(obj) ? obj : _.values(obj); + var length = set.length; + var shuffled = Array(length); + for (var index = 0, rand; index < length; index++) { + rand = _.random(0, index); + if (rand !== index) shuffled[index] = shuffled[rand]; + shuffled[rand] = set[index]; + } + return shuffled; + }; + + // Sample **n** random values from a collection. + // If **n** is not specified, returns a single random element. + // The internal `guard` argument allows it to work with `map`. + _.sample = function(obj, n, guard) { + if (n == null || guard) { + if (!isArrayLike(obj)) obj = _.values(obj); + return obj[_.random(obj.length - 1)]; + } + return _.shuffle(obj).slice(0, Math.max(0, n)); + }; + + // Sort the object's values by a criterion produced by an iteratee. + _.sortBy = function(obj, iteratee, context) { + iteratee = cb(iteratee, context); + return _.pluck(_.map(obj, function(value, index, list) { + return { + value: value, + index: index, + criteria: iteratee(value, index, list) + }; + }).sort(function(left, right) { + var a = left.criteria; + var b = right.criteria; + if (a !== b) { + if (a > b || a === void 0) return 1; + if (a < b || b === void 0) return -1; + } + return left.index - right.index; + }), 'value'); + }; + + // An internal function used for aggregate "group by" operations. + var group = function(behavior) { + return function(obj, iteratee, context) { + var result = {}; + iteratee = cb(iteratee, context); + _.each(obj, function(value, index) { + var key = iteratee(value, index, obj); + behavior(result, value, key); + }); + return result; + }; + }; + + // Groups the object's values by a criterion. Pass either a string attribute + // to group by, or a function that returns the criterion. + _.groupBy = group(function(result, value, key) { + if (_.has(result, key)) result[key].push(value); else result[key] = [value]; + }); + + // Indexes the object's values by a criterion, similar to `groupBy`, but for + // when you know that your index values will be unique. + _.indexBy = group(function(result, value, key) { + result[key] = value; + }); + + // Counts instances of an object that group by a certain criterion. Pass + // either a string attribute to count by, or a function that returns the + // criterion. + _.countBy = group(function(result, value, key) { + if (_.has(result, key)) result[key]++; else result[key] = 1; + }); + + // Safely create a real, live array from anything iterable. + _.toArray = function(obj) { + if (!obj) return []; + if (_.isArray(obj)) return slice.call(obj); + if (isArrayLike(obj)) return _.map(obj, _.identity); + return _.values(obj); + }; + + // Return the number of elements in an object. + _.size = function(obj) { + if (obj == null) return 0; + return isArrayLike(obj) ? obj.length : _.keys(obj).length; + }; + + // Split a collection into two arrays: one whose elements all satisfy the given + // predicate, and one whose elements all do not satisfy the predicate. + _.partition = function(obj, predicate, context) { + predicate = cb(predicate, context); + var pass = [], fail = []; + _.each(obj, function(value, key, obj) { + (predicate(value, key, obj) ? pass : fail).push(value); + }); + return [pass, fail]; + }; + + // Array Functions + // --------------- + + // Get the first element of an array. Passing **n** will return the first N + // values in the array. Aliased as `head` and `take`. The **guard** check + // allows it to work with `_.map`. + _.first = _.head = _.take = function(array, n, guard) { + if (array == null) return void 0; + if (n == null || guard) return array[0]; + return _.initial(array, array.length - n); + }; + + // Returns everything but the last entry of the array. Especially useful on + // the arguments object. Passing **n** will return all the values in + // the array, excluding the last N. + _.initial = function(array, n, guard) { + return slice.call(array, 0, Math.max(0, array.length - (n == null || guard ? 1 : n))); + }; + + // Get the last element of an array. Passing **n** will return the last N + // values in the array. + _.last = function(array, n, guard) { + if (array == null) return void 0; + if (n == null || guard) return array[array.length - 1]; + return _.rest(array, Math.max(0, array.length - n)); + }; + + // Returns everything but the first entry of the array. Aliased as `tail` and `drop`. + // Especially useful on the arguments object. Passing an **n** will return + // the rest N values in the array. + _.rest = _.tail = _.drop = function(array, n, guard) { + return slice.call(array, n == null || guard ? 1 : n); + }; + + // Trim out all falsy values from an array. + _.compact = function(array) { + return _.filter(array, _.identity); + }; + + // Internal implementation of a recursive `flatten` function. + var flatten = function(input, shallow, strict, startIndex) { + var output = [], idx = 0; + for (var i = startIndex || 0, length = getLength(input); i < length; i++) { + var value = input[i]; + if (isArrayLike(value) && (_.isArray(value) || _.isArguments(value))) { + //flatten current level of array or arguments object + if (!shallow) value = flatten(value, shallow, strict); + var j = 0, len = value.length; + output.length += len; + while (j < len) { + output[idx++] = value[j++]; + } + } else if (!strict) { + output[idx++] = value; + } + } + return output; + }; + + // Flatten out an array, either recursively (by default), or just one level. + _.flatten = function(array, shallow) { + return flatten(array, shallow, false); + }; + + // Return a version of the array that does not contain the specified value(s). + _.without = function(array) { + return _.difference(array, slice.call(arguments, 1)); + }; + + // Produce a duplicate-free version of the array. If the array has already + // been sorted, you have the option of using a faster algorithm. + // Aliased as `unique`. + _.uniq = _.unique = function(array, isSorted, iteratee, context) { + if (!_.isBoolean(isSorted)) { + context = iteratee; + iteratee = isSorted; + isSorted = false; + } + if (iteratee != null) iteratee = cb(iteratee, context); + var result = []; + var seen = []; + for (var i = 0, length = getLength(array); i < length; i++) { + var value = array[i], + computed = iteratee ? iteratee(value, i, array) : value; + if (isSorted) { + if (!i || seen !== computed) result.push(value); + seen = computed; + } else if (iteratee) { + if (!_.contains(seen, computed)) { + seen.push(computed); + result.push(value); + } + } else if (!_.contains(result, value)) { + result.push(value); + } + } + return result; + }; + + // Produce an array that contains the union: each distinct element from all of + // the passed-in arrays. + _.union = function() { + return _.uniq(flatten(arguments, true, true)); + }; + + // Produce an array that contains every item shared between all the + // passed-in arrays. + _.intersection = function(array) { + var result = []; + var argsLength = arguments.length; + for (var i = 0, length = getLength(array); i < length; i++) { + var item = array[i]; + if (_.contains(result, item)) continue; + for (var j = 1; j < argsLength; j++) { + if (!_.contains(arguments[j], item)) break; + } + if (j === argsLength) result.push(item); + } + return result; + }; + + // Take the difference between one array and a number of other arrays. + // Only the elements present in just the first array will remain. + _.difference = function(array) { + var rest = flatten(arguments, true, true, 1); + return _.filter(array, function(value){ + return !_.contains(rest, value); + }); + }; + + // Zip together multiple lists into a single array -- elements that share + // an index go together. + _.zip = function() { + return _.unzip(arguments); + }; + + // Complement of _.zip. Unzip accepts an array of arrays and groups + // each array's elements on shared indices + _.unzip = function(array) { + var length = array && _.max(array, getLength).length || 0; + var result = Array(length); + + for (var index = 0; index < length; index++) { + result[index] = _.pluck(array, index); + } + return result; + }; + + // Converts lists into objects. Pass either a single array of `[key, value]` + // pairs, or two parallel arrays of the same length -- one of keys, and one of + // the corresponding values. + _.object = function(list, values) { + var result = {}; + for (var i = 0, length = getLength(list); i < length; i++) { + if (values) { + result[list[i]] = values[i]; + } else { + result[list[i][0]] = list[i][1]; + } + } + return result; + }; + + // Generator function to create the findIndex and findLastIndex functions + function createPredicateIndexFinder(dir) { + return function(array, predicate, context) { + predicate = cb(predicate, context); + var length = getLength(array); + var index = dir > 0 ? 0 : length - 1; + for (; index >= 0 && index < length; index += dir) { + if (predicate(array[index], index, array)) return index; + } + return -1; + }; + } + + // Returns the first index on an array-like that passes a predicate test + _.findIndex = createPredicateIndexFinder(1); + _.findLastIndex = createPredicateIndexFinder(-1); + + // Use a comparator function to figure out the smallest index at which + // an object should be inserted so as to maintain order. Uses binary search. + _.sortedIndex = function(array, obj, iteratee, context) { + iteratee = cb(iteratee, context, 1); + var value = iteratee(obj); + var low = 0, high = getLength(array); + while (low < high) { + var mid = Math.floor((low + high) / 2); + if (iteratee(array[mid]) < value) low = mid + 1; else high = mid; + } + return low; + }; + + // Generator function to create the indexOf and lastIndexOf functions + function createIndexFinder(dir, predicateFind, sortedIndex) { + return function(array, item, idx) { + var i = 0, length = getLength(array); + if (typeof idx == 'number') { + if (dir > 0) { + i = idx >= 0 ? idx : Math.max(idx + length, i); + } else { + length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1; + } + } else if (sortedIndex && idx && length) { + idx = sortedIndex(array, item); + return array[idx] === item ? idx : -1; + } + if (item !== item) { + idx = predicateFind(slice.call(array, i, length), _.isNaN); + return idx >= 0 ? idx + i : -1; + } + for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) { + if (array[idx] === item) return idx; + } + return -1; + }; + } + + // Return the position of the first occurrence of an item in an array, + // or -1 if the item is not included in the array. + // If the array is large and already in sort order, pass `true` + // for **isSorted** to use binary search. + _.indexOf = createIndexFinder(1, _.findIndex, _.sortedIndex); + _.lastIndexOf = createIndexFinder(-1, _.findLastIndex); + + // Generate an integer Array containing an arithmetic progression. A port of + // the native Python `range()` function. See + // [the Python documentation](http://docs.python.org/library/functions.html#range). + _.range = function(start, stop, step) { + if (stop == null) { + stop = start || 0; + start = 0; + } + step = step || 1; + + var length = Math.max(Math.ceil((stop - start) / step), 0); + var range = Array(length); + + for (var idx = 0; idx < length; idx++, start += step) { + range[idx] = start; + } + + return range; + }; + + // Function (ahem) Functions + // ------------------ + + // Determines whether to execute a function as a constructor + // or a normal function with the provided arguments + var executeBound = function(sourceFunc, boundFunc, context, callingContext, args) { + if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args); + var self = baseCreate(sourceFunc.prototype); + var result = sourceFunc.apply(self, args); + if (_.isObject(result)) return result; + return self; + }; + + // Create a function bound to a given object (assigning `this`, and arguments, + // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if + // available. + _.bind = function(func, context) { + if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); + if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function'); + var args = slice.call(arguments, 2); + var bound = function() { + return executeBound(func, bound, context, this, args.concat(slice.call(arguments))); + }; + return bound; + }; + + // Partially apply a function by creating a version that has had some of its + // arguments pre-filled, without changing its dynamic `this` context. _ acts + // as a placeholder, allowing any combination of arguments to be pre-filled. + _.partial = function(func) { + var boundArgs = slice.call(arguments, 1); + var bound = function() { + var position = 0, length = boundArgs.length; + var args = Array(length); + for (var i = 0; i < length; i++) { + args[i] = boundArgs[i] === _ ? arguments[position++] : boundArgs[i]; + } + while (position < arguments.length) args.push(arguments[position++]); + return executeBound(func, bound, this, this, args); + }; + return bound; + }; + + // Bind a number of an object's methods to that object. Remaining arguments + // are the method names to be bound. Useful for ensuring that all callbacks + // defined on an object belong to it. + _.bindAll = function(obj) { + var i, length = arguments.length, key; + if (length <= 1) throw new Error('bindAll must be passed function names'); + for (i = 1; i < length; i++) { + key = arguments[i]; + obj[key] = _.bind(obj[key], obj); + } + return obj; + }; + + // Memoize an expensive function by storing its results. + _.memoize = function(func, hasher) { + var memoize = function(key) { + var cache = memoize.cache; + var address = '' + (hasher ? hasher.apply(this, arguments) : key); + if (!_.has(cache, address)) cache[address] = func.apply(this, arguments); + return cache[address]; + }; + memoize.cache = {}; + return memoize; + }; + + // Delays a function for the given number of milliseconds, and then calls + // it with the arguments supplied. + _.delay = function(func, wait) { + var args = slice.call(arguments, 2); + return setTimeout(function(){ + return func.apply(null, args); + }, wait); + }; + + // Defers a function, scheduling it to run after the current call stack has + // cleared. + _.defer = _.partial(_.delay, _, 1); + + // Returns a function, that, when invoked, will only be triggered at most once + // during a given window of time. Normally, the throttled function will run + // as much as it can, without ever going more than once per `wait` duration; + // but if you'd like to disable the execution on the leading edge, pass + // `{leading: false}`. To disable execution on the trailing edge, ditto. + _.throttle = function(func, wait, options) { + var context, args, result; + var timeout = null; + var previous = 0; + if (!options) options = {}; + var later = function() { + previous = options.leading === false ? 0 : _.now(); + timeout = null; + result = func.apply(context, args); + if (!timeout) context = args = null; + }; + return function() { + var now = _.now(); + if (!previous && options.leading === false) previous = now; + var remaining = wait - (now - previous); + context = this; + args = arguments; + if (remaining <= 0 || remaining > wait) { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + previous = now; + result = func.apply(context, args); + if (!timeout) context = args = null; + } else if (!timeout && options.trailing !== false) { + timeout = setTimeout(later, remaining); + } + return result; + }; + }; + + // Returns a function, that, as long as it continues to be invoked, will not + // be triggered. The function will be called after it stops being called for + // N milliseconds. If `immediate` is passed, trigger the function on the + // leading edge, instead of the trailing. + _.debounce = function(func, wait, immediate) { + var timeout, args, context, timestamp, result; + + var later = function() { + var last = _.now() - timestamp; + + if (last < wait && last >= 0) { + timeout = setTimeout(later, wait - last); + } else { + timeout = null; + if (!immediate) { + result = func.apply(context, args); + if (!timeout) context = args = null; + } + } + }; + + return function() { + context = this; + args = arguments; + timestamp = _.now(); + var callNow = immediate && !timeout; + if (!timeout) timeout = setTimeout(later, wait); + if (callNow) { + result = func.apply(context, args); + context = args = null; + } + + return result; + }; + }; + + // Returns the first function passed as an argument to the second, + // allowing you to adjust arguments, run code before and after, and + // conditionally execute the original function. + _.wrap = function(func, wrapper) { + return _.partial(wrapper, func); + }; + + // Returns a negated version of the passed-in predicate. + _.negate = function(predicate) { + return function() { + return !predicate.apply(this, arguments); + }; + }; + + // Returns a function that is the composition of a list of functions, each + // consuming the return value of the function that follows. + _.compose = function() { + var args = arguments; + var start = args.length - 1; + return function() { + var i = start; + var result = args[start].apply(this, arguments); + while (i--) result = args[i].call(this, result); + return result; + }; + }; + + // Returns a function that will only be executed on and after the Nth call. + _.after = function(times, func) { + return function() { + if (--times < 1) { + return func.apply(this, arguments); + } + }; + }; + + // Returns a function that will only be executed up to (but not including) the Nth call. + _.before = function(times, func) { + var memo; + return function() { + if (--times > 0) { + memo = func.apply(this, arguments); + } + if (times <= 1) func = null; + return memo; + }; + }; + + // Returns a function that will be executed at most one time, no matter how + // often you call it. Useful for lazy initialization. + _.once = _.partial(_.before, 2); + + // Object Functions + // ---------------- + + // Keys in IE < 9 that won't be iterated by `for key in ...` and thus missed. + var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString'); + var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString', + 'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString']; + + function collectNonEnumProps(obj, keys) { + var nonEnumIdx = nonEnumerableProps.length; + var constructor = obj.constructor; + var proto = (_.isFunction(constructor) && constructor.prototype) || ObjProto; + + // Constructor is a special case. + var prop = 'constructor'; + if (_.has(obj, prop) && !_.contains(keys, prop)) keys.push(prop); + + while (nonEnumIdx--) { + prop = nonEnumerableProps[nonEnumIdx]; + if (prop in obj && obj[prop] !== proto[prop] && !_.contains(keys, prop)) { + keys.push(prop); + } + } + } + + // Retrieve the names of an object's own properties. + // Delegates to **ECMAScript 5**'s native `Object.keys` + _.keys = function(obj) { + if (!_.isObject(obj)) return []; + if (nativeKeys) return nativeKeys(obj); + var keys = []; + for (var key in obj) if (_.has(obj, key)) keys.push(key); + // Ahem, IE < 9. + if (hasEnumBug) collectNonEnumProps(obj, keys); + return keys; + }; + + // Retrieve all the property names of an object. + _.allKeys = function(obj) { + if (!_.isObject(obj)) return []; + var keys = []; + for (var key in obj) keys.push(key); + // Ahem, IE < 9. + if (hasEnumBug) collectNonEnumProps(obj, keys); + return keys; + }; + + // Retrieve the values of an object's properties. + _.values = function(obj) { + var keys = _.keys(obj); + var length = keys.length; + var values = Array(length); + for (var i = 0; i < length; i++) { + values[i] = obj[keys[i]]; + } + return values; + }; + + // Returns the results of applying the iteratee to each element of the object + // In contrast to _.map it returns an object + _.mapObject = function(obj, iteratee, context) { + iteratee = cb(iteratee, context); + var keys = _.keys(obj), + length = keys.length, + results = {}, + currentKey; + for (var index = 0; index < length; index++) { + currentKey = keys[index]; + results[currentKey] = iteratee(obj[currentKey], currentKey, obj); + } + return results; + }; + + // Convert an object into a list of `[key, value]` pairs. + _.pairs = function(obj) { + var keys = _.keys(obj); + var length = keys.length; + var pairs = Array(length); + for (var i = 0; i < length; i++) { + pairs[i] = [keys[i], obj[keys[i]]]; + } + return pairs; + }; + + // Invert the keys and values of an object. The values must be serializable. + _.invert = function(obj) { + var result = {}; + var keys = _.keys(obj); + for (var i = 0, length = keys.length; i < length; i++) { + result[obj[keys[i]]] = keys[i]; + } + return result; + }; + + // Return a sorted list of the function names available on the object. + // Aliased as `methods` + _.functions = _.methods = function(obj) { + var names = []; + for (var key in obj) { + if (_.isFunction(obj[key])) names.push(key); + } + return names.sort(); + }; + + // Extend a given object with all the properties in passed-in object(s). + _.extend = createAssigner(_.allKeys); + + // Assigns a given object with all the own properties in the passed-in object(s) + // (https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) + _.extendOwn = _.assign = createAssigner(_.keys); + + // Returns the first key on an object that passes a predicate test + _.findKey = function(obj, predicate, context) { + predicate = cb(predicate, context); + var keys = _.keys(obj), key; + for (var i = 0, length = keys.length; i < length; i++) { + key = keys[i]; + if (predicate(obj[key], key, obj)) return key; + } + }; + + // Return a copy of the object only containing the whitelisted properties. + _.pick = function(object, oiteratee, context) { + var result = {}, obj = object, iteratee, keys; + if (obj == null) return result; + if (_.isFunction(oiteratee)) { + keys = _.allKeys(obj); + iteratee = optimizeCb(oiteratee, context); + } else { + keys = flatten(arguments, false, false, 1); + iteratee = function(value, key, obj) { return key in obj; }; + obj = Object(obj); + } + for (var i = 0, length = keys.length; i < length; i++) { + var key = keys[i]; + var value = obj[key]; + if (iteratee(value, key, obj)) result[key] = value; + } + return result; + }; + + // Return a copy of the object without the blacklisted properties. + _.omit = function(obj, iteratee, context) { + if (_.isFunction(iteratee)) { + iteratee = _.negate(iteratee); + } else { + var keys = _.map(flatten(arguments, false, false, 1), String); + iteratee = function(value, key) { + return !_.contains(keys, key); + }; + } + return _.pick(obj, iteratee, context); + }; + + // Fill in a given object with default properties. + _.defaults = createAssigner(_.allKeys, true); + + // Creates an object that inherits from the given prototype object. + // If additional properties are provided then they will be added to the + // created object. + _.create = function(prototype, props) { + var result = baseCreate(prototype); + if (props) _.extendOwn(result, props); + return result; + }; + + // Create a (shallow-cloned) duplicate of an object. + _.clone = function(obj) { + if (!_.isObject(obj)) return obj; + return _.isArray(obj) ? obj.slice() : _.extend({}, obj); + }; + + // Invokes interceptor with the obj, and then returns obj. + // The primary purpose of this method is to "tap into" a method chain, in + // order to perform operations on intermediate results within the chain. + _.tap = function(obj, interceptor) { + interceptor(obj); + return obj; + }; + + // Returns whether an object has a given set of `key:value` pairs. + _.isMatch = function(object, attrs) { + var keys = _.keys(attrs), length = keys.length; + if (object == null) return !length; + var obj = Object(object); + for (var i = 0; i < length; i++) { + var key = keys[i]; + if (attrs[key] !== obj[key] || !(key in obj)) return false; + } + return true; + }; + + + // Internal recursive comparison function for `isEqual`. + var eq = function(a, b, aStack, bStack) { + // Identical objects are equal. `0 === -0`, but they aren't identical. + // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal). + if (a === b) return a !== 0 || 1 / a === 1 / b; + // A strict comparison is necessary because `null == undefined`. + if (a == null || b == null) return a === b; + // Unwrap any wrapped objects. + if (a instanceof _) a = a._wrapped; + if (b instanceof _) b = b._wrapped; + // Compare `[[Class]]` names. + var className = toString.call(a); + if (className !== toString.call(b)) return false; + switch (className) { + // Strings, numbers, regular expressions, dates, and booleans are compared by value. + case '[object RegExp]': + // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i') + case '[object String]': + // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is + // equivalent to `new String("5")`. + return '' + a === '' + b; + case '[object Number]': + // `NaN`s are equivalent, but non-reflexive. + // Object(NaN) is equivalent to NaN + if (+a !== +a) return +b !== +b; + // An `egal` comparison is performed for other numeric values. + return +a === 0 ? 1 / +a === 1 / b : +a === +b; + case '[object Date]': + case '[object Boolean]': + // Coerce dates and booleans to numeric primitive values. Dates are compared by their + // millisecond representations. Note that invalid dates with millisecond representations + // of `NaN` are not equivalent. + return +a === +b; + } + + var areArrays = className === '[object Array]'; + if (!areArrays) { + if (typeof a != 'object' || typeof b != 'object') return false; + + // Objects with different constructors are not equivalent, but `Object`s or `Array`s + // from different frames are. + var aCtor = a.constructor, bCtor = b.constructor; + if (aCtor !== bCtor && !(_.isFunction(aCtor) && aCtor instanceof aCtor && + _.isFunction(bCtor) && bCtor instanceof bCtor) + && ('constructor' in a && 'constructor' in b)) { + return false; + } + } + // Assume equality for cyclic structures. The algorithm for detecting cyclic + // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. + + // Initializing stack of traversed objects. + // It's done here since we only need them for objects and arrays comparison. + aStack = aStack || []; + bStack = bStack || []; + var length = aStack.length; + while (length--) { + // Linear search. Performance is inversely proportional to the number of + // unique nested structures. + if (aStack[length] === a) return bStack[length] === b; + } + + // Add the first object to the stack of traversed objects. + aStack.push(a); + bStack.push(b); + + // Recursively compare objects and arrays. + if (areArrays) { + // Compare array lengths to determine if a deep comparison is necessary. + length = a.length; + if (length !== b.length) return false; + // Deep compare the contents, ignoring non-numeric properties. + while (length--) { + if (!eq(a[length], b[length], aStack, bStack)) return false; + } + } else { + // Deep compare objects. + var keys = _.keys(a), key; + length = keys.length; + // Ensure that both objects contain the same number of properties before comparing deep equality. + if (_.keys(b).length !== length) return false; + while (length--) { + // Deep compare each member + key = keys[length]; + if (!(_.has(b, key) && eq(a[key], b[key], aStack, bStack))) return false; + } + } + // Remove the first object from the stack of traversed objects. + aStack.pop(); + bStack.pop(); + return true; + }; + + // Perform a deep comparison to check if two objects are equal. + _.isEqual = function(a, b) { + return eq(a, b); + }; + + // Is a given array, string, or object empty? + // An "empty" object has no enumerable own-properties. + _.isEmpty = function(obj) { + if (obj == null) return true; + if (isArrayLike(obj) && (_.isArray(obj) || _.isString(obj) || _.isArguments(obj))) return obj.length === 0; + return _.keys(obj).length === 0; + }; + + // Is a given value a DOM element? + _.isElement = function(obj) { + return !!(obj && obj.nodeType === 1); + }; + + // Is a given value an array? + // Delegates to ECMA5's native Array.isArray + _.isArray = nativeIsArray || function(obj) { + return toString.call(obj) === '[object Array]'; + }; + + // Is a given variable an object? + _.isObject = function(obj) { + var type = typeof obj; + return type === 'function' || type === 'object' && !!obj; + }; + + // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp, isError. + _.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp', 'Error'], function(name) { + _['is' + name] = function(obj) { + return toString.call(obj) === '[object ' + name + ']'; + }; + }); + + // Define a fallback version of the method in browsers (ahem, IE < 9), where + // there isn't any inspectable "Arguments" type. + if (!_.isArguments(arguments)) { + _.isArguments = function(obj) { + return _.has(obj, 'callee'); + }; + } + + // Optimize `isFunction` if appropriate. Work around some typeof bugs in old v8, + // IE 11 (#1621), and in Safari 8 (#1929). + if (typeof /./ != 'function' && typeof Int8Array != 'object') { + _.isFunction = function(obj) { + return typeof obj == 'function' || false; + }; + } + + // Is a given object a finite number? + _.isFinite = function(obj) { + return isFinite(obj) && !isNaN(parseFloat(obj)); + }; + + // Is the given value `NaN`? (NaN is the only number which does not equal itself). + _.isNaN = function(obj) { + return _.isNumber(obj) && obj !== +obj; + }; + + // Is a given value a boolean? + _.isBoolean = function(obj) { + return obj === true || obj === false || toString.call(obj) === '[object Boolean]'; + }; + + // Is a given value equal to null? + _.isNull = function(obj) { + return obj === null; + }; + + // Is a given variable undefined? + _.isUndefined = function(obj) { + return obj === void 0; + }; + + // Shortcut function for checking if an object has a given property directly + // on itself (in other words, not on a prototype). + _.has = function(obj, key) { + return obj != null && hasOwnProperty.call(obj, key); + }; + + // Utility Functions + // ----------------- + + // Run Underscore.js in *noConflict* mode, returning the `_` variable to its + // previous owner. Returns a reference to the Underscore object. + _.noConflict = function() { + root._ = previousUnderscore; + return this; + }; + + // Keep the identity function around for default iteratees. + _.identity = function(value) { + return value; + }; + + // Predicate-generating functions. Often useful outside of Underscore. + _.constant = function(value) { + return function() { + return value; + }; + }; + + _.noop = function(){}; + + _.property = property; + + // Generates a function for a given object that returns a given property. + _.propertyOf = function(obj) { + return obj == null ? function(){} : function(key) { + return obj[key]; + }; + }; + + // Returns a predicate for checking whether an object has a given set of + // `key:value` pairs. + _.matcher = _.matches = function(attrs) { + attrs = _.extendOwn({}, attrs); + return function(obj) { + return _.isMatch(obj, attrs); + }; + }; + + // Run a function **n** times. + _.times = function(n, iteratee, context) { + var accum = Array(Math.max(0, n)); + iteratee = optimizeCb(iteratee, context, 1); + for (var i = 0; i < n; i++) accum[i] = iteratee(i); + return accum; + }; + + // Return a random integer between min and max (inclusive). + _.random = function(min, max) { + if (max == null) { + max = min; + min = 0; + } + return min + Math.floor(Math.random() * (max - min + 1)); + }; + + // A (possibly faster) way to get the current timestamp as an integer. + _.now = Date.now || function() { + return new Date().getTime(); + }; + + // List of HTML entities for escaping. + var escapeMap = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '`': '`' + }; + var unescapeMap = _.invert(escapeMap); + + // Functions for escaping and unescaping strings to/from HTML interpolation. + var createEscaper = function(map) { + var escaper = function(match) { + return map[match]; + }; + // Regexes for identifying a key that needs to be escaped + var source = '(?:' + _.keys(map).join('|') + ')'; + var testRegexp = RegExp(source); + var replaceRegexp = RegExp(source, 'g'); + return function(string) { + string = string == null ? '' : '' + string; + return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string; + }; + }; + _.escape = createEscaper(escapeMap); + _.unescape = createEscaper(unescapeMap); + + // If the value of the named `property` is a function then invoke it with the + // `object` as context; otherwise, return it. + _.result = function(object, property, fallback) { + var value = object == null ? void 0 : object[property]; + if (value === void 0) { + value = fallback; + } + return _.isFunction(value) ? value.call(object) : value; + }; + + // Generate a unique integer id (unique within the entire client session). + // Useful for temporary DOM ids. + var idCounter = 0; + _.uniqueId = function(prefix) { + var id = ++idCounter + ''; + return prefix ? prefix + id : id; + }; + + // By default, Underscore uses ERB-style template delimiters, change the + // following template settings to use alternative delimiters. + _.templateSettings = { + evaluate : /<%([\s\S]+?)%>/g, + interpolate : /<%=([\s\S]+?)%>/g, + escape : /<%-([\s\S]+?)%>/g + }; + + // When customizing `templateSettings`, if you don't want to define an + // interpolation, evaluation or escaping regex, we need one that is + // guaranteed not to match. + var noMatch = /(.)^/; + + // Certain characters need to be escaped so that they can be put into a + // string literal. + var escapes = { + "'": "'", + '\\': '\\', + '\r': 'r', + '\n': 'n', + '\u2028': 'u2028', + '\u2029': 'u2029' + }; + + var escaper = /\\|'|\r|\n|\u2028|\u2029/g; + + var escapeChar = function(match) { + return '\\' + escapes[match]; + }; + + // JavaScript micro-templating, similar to John Resig's implementation. + // Underscore templating handles arbitrary delimiters, preserves whitespace, + // and correctly escapes quotes within interpolated code. + // NB: `oldSettings` only exists for backwards compatibility. + _.template = function(text, settings, oldSettings) { + if (!settings && oldSettings) settings = oldSettings; + settings = _.defaults({}, settings, _.templateSettings); + + // Combine delimiters into one regular expression via alternation. + var matcher = RegExp([ + (settings.escape || noMatch).source, + (settings.interpolate || noMatch).source, + (settings.evaluate || noMatch).source + ].join('|') + '|$', 'g'); + + // Compile the template source, escaping string literals appropriately. + var index = 0; + var source = "__p+='"; + text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { + source += text.slice(index, offset).replace(escaper, escapeChar); + index = offset + match.length; + + if (escape) { + source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; + } else if (interpolate) { + source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; + } else if (evaluate) { + source += "';\n" + evaluate + "\n__p+='"; + } + + // Adobe VMs need the match returned to produce the correct offest. + return match; + }); + source += "';\n"; + + // If a variable is not specified, place data values in local scope. + if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; + + source = "var __t,__p='',__j=Array.prototype.join," + + "print=function(){__p+=__j.call(arguments,'');};\n" + + source + 'return __p;\n'; + + try { + var render = new Function(settings.variable || 'obj', '_', source); + } catch (e) { + e.source = source; + throw e; + } + + var template = function(data) { + return render.call(this, data, _); + }; + + // Provide the compiled source as a convenience for precompilation. + var argument = settings.variable || 'obj'; + template.source = 'function(' + argument + '){\n' + source + '}'; + + return template; + }; + + // Add a "chain" function. Start chaining a wrapped Underscore object. + _.chain = function(obj) { + var instance = _(obj); + instance._chain = true; + return instance; + }; + + // OOP + // --------------- + // If Underscore is called as a function, it returns a wrapped object that + // can be used OO-style. This wrapper holds altered versions of all the + // underscore functions. Wrapped objects may be chained. + + // Helper function to continue chaining intermediate results. + var result = function(instance, obj) { + return instance._chain ? _(obj).chain() : obj; + }; + + // Add your own custom functions to the Underscore object. + _.mixin = function(obj) { + _.each(_.functions(obj), function(name) { + var func = _[name] = obj[name]; + _.prototype[name] = function() { + var args = [this._wrapped]; + push.apply(args, arguments); + return result(this, func.apply(_, args)); + }; + }); + }; + + // Add all of the Underscore functions to the wrapper object. + _.mixin(_); + + // Add all mutator Array functions to the wrapper. + _.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { + var method = ArrayProto[name]; + _.prototype[name] = function() { + var obj = this._wrapped; + method.apply(obj, arguments); + if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0]; + return result(this, obj); + }; + }); + + // Add all accessor Array functions to the wrapper. + _.each(['concat', 'join', 'slice'], function(name) { + var method = ArrayProto[name]; + _.prototype[name] = function() { + return result(this, method.apply(this._wrapped, arguments)); + }; + }); + + // Extracts the result from a wrapped and chained object. + _.prototype.value = function() { + return this._wrapped; + }; + + // Provide unwrapping proxy for some methods used in engine operations + // such as arithmetic and JSON stringification. + _.prototype.valueOf = _.prototype.toJSON = _.prototype.value; + + _.prototype.toString = function() { + return '' + this._wrapped; + }; + + // AMD registration happens at the end for compatibility with AMD loaders + // that may not enforce next-turn semantics on modules. Even though general + // practice for AMD registration is to be anonymous, underscore registers + // as a named module because, like jQuery, it is a base library that is + // popular enough to be bundled in a third party lib, but not be part of + // an AMD load request. Those cases could generate an error when an + // anonymous define() is called outside of a loader request. + if (typeof define === 'function' && define.amd) { + define('underscore', [], function() { + return _; + }); + } +}.call(this)); + +},{}],26:[function(require,module,exports){ +arguments[4][19][0].apply(exports,arguments) +},{"dup":19}],27:[function(require,module,exports){ +module.exports = function isBuffer(arg) { + return arg && typeof arg === 'object' + && typeof arg.copy === 'function' + && typeof arg.fill === 'function' + && typeof arg.readUInt8 === 'function'; +} +},{}],28:[function(require,module,exports){ +(function (process,global){ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +var formatRegExp = /%[sdj%]/g; +exports.format = function(f) { + if (!isString(f)) { + var objects = []; + for (var i = 0; i < arguments.length; i++) { + objects.push(inspect(arguments[i])); + } + return objects.join(' '); + } + + var i = 1; + var args = arguments; + var len = args.length; + var str = String(f).replace(formatRegExp, function(x) { + if (x === '%%') return '%'; + if (i >= len) return x; + switch (x) { + case '%s': return String(args[i++]); + case '%d': return Number(args[i++]); + case '%j': + try { + return JSON.stringify(args[i++]); + } catch (_) { + return '[Circular]'; + } + default: + return x; + } + }); + for (var x = args[i]; i < len; x = args[++i]) { + if (isNull(x) || !isObject(x)) { + str += ' ' + x; + } else { + str += ' ' + inspect(x); + } + } + return str; +}; + + +// Mark that a method should not be used. +// Returns a modified function which warns once by default. +// If --no-deprecation is set, then it is a no-op. +exports.deprecate = function(fn, msg) { + // Allow for deprecating things in the process of starting up. + if (isUndefined(global.process)) { + return function() { + return exports.deprecate(fn, msg).apply(this, arguments); + }; + } + + if (process.noDeprecation === true) { + return fn; + } + + var warned = false; + function deprecated() { + if (!warned) { + if (process.throwDeprecation) { + throw new Error(msg); + } else if (process.traceDeprecation) { + console.trace(msg); + } else { + console.error(msg); + } + warned = true; + } + return fn.apply(this, arguments); + } + + return deprecated; +}; + + +var debugs = {}; +var debugEnviron; +exports.debuglog = function(set) { + if (isUndefined(debugEnviron)) + debugEnviron = process.env.NODE_DEBUG || ''; + set = set.toUpperCase(); + if (!debugs[set]) { + if (new RegExp('\\b' + set + '\\b', 'i').test(debugEnviron)) { + var pid = process.pid; + debugs[set] = function() { + var msg = exports.format.apply(exports, arguments); + console.error('%s %d: %s', set, pid, msg); + }; + } else { + debugs[set] = function() {}; + } + } + return debugs[set]; +}; + + +/** + * Echos the value of a value. Trys to print the value out + * in the best way possible given the different types. + * + * @param {Object} obj The object to print out. + * @param {Object} opts Optional options object that alters the output. + */ +/* legacy: obj, showHidden, depth, colors*/ +function inspect(obj, opts) { + // default options + var ctx = { + seen: [], + stylize: stylizeNoColor + }; + // legacy... + if (arguments.length >= 3) ctx.depth = arguments[2]; + if (arguments.length >= 4) ctx.colors = arguments[3]; + if (isBoolean(opts)) { + // legacy... + ctx.showHidden = opts; + } else if (opts) { + // got an "options" object + exports._extend(ctx, opts); + } + // set default options + if (isUndefined(ctx.showHidden)) ctx.showHidden = false; + if (isUndefined(ctx.depth)) ctx.depth = 2; + if (isUndefined(ctx.colors)) ctx.colors = false; + if (isUndefined(ctx.customInspect)) ctx.customInspect = true; + if (ctx.colors) ctx.stylize = stylizeWithColor; + return formatValue(ctx, obj, ctx.depth); +} +exports.inspect = inspect; + + +// http://en.wikipedia.org/wiki/ANSI_escape_code#graphics +inspect.colors = { + 'bold' : [1, 22], + 'italic' : [3, 23], + 'underline' : [4, 24], + 'inverse' : [7, 27], + 'white' : [37, 39], + 'grey' : [90, 39], + 'black' : [30, 39], + 'blue' : [34, 39], + 'cyan' : [36, 39], + 'green' : [32, 39], + 'magenta' : [35, 39], + 'red' : [31, 39], + 'yellow' : [33, 39] +}; + +// Don't use 'blue' not visible on cmd.exe +inspect.styles = { + 'special': 'cyan', + 'number': 'yellow', + 'boolean': 'yellow', + 'undefined': 'grey', + 'null': 'bold', + 'string': 'green', + 'date': 'magenta', + // "name": intentionally not styling + 'regexp': 'red' +}; + + +function stylizeWithColor(str, styleType) { + var style = inspect.styles[styleType]; + + if (style) { + return '\u001b[' + inspect.colors[style][0] + 'm' + str + + '\u001b[' + inspect.colors[style][1] + 'm'; + } else { + return str; + } +} + + +function stylizeNoColor(str, styleType) { + return str; +} + + +function arrayToHash(array) { + var hash = {}; + + array.forEach(function(val, idx) { + hash[val] = true; + }); + + return hash; +} + + +function formatValue(ctx, value, recurseTimes) { + // Provide a hook for user-specified inspect functions. + // Check that value is an object with an inspect function on it + if (ctx.customInspect && + value && + isFunction(value.inspect) && + // Filter out the util module, it's inspect function is special + value.inspect !== exports.inspect && + // Also filter out any prototype objects using the circular check. + !(value.constructor && value.constructor.prototype === value)) { + var ret = value.inspect(recurseTimes, ctx); + if (!isString(ret)) { + ret = formatValue(ctx, ret, recurseTimes); + } + return ret; + } + + // Primitive types cannot have properties + var primitive = formatPrimitive(ctx, value); + if (primitive) { + return primitive; + } + + // Look up the keys of the object. + var keys = Object.keys(value); + var visibleKeys = arrayToHash(keys); + + if (ctx.showHidden) { + keys = Object.getOwnPropertyNames(value); + } + + // IE doesn't make error fields non-enumerable + // http://msdn.microsoft.com/en-us/library/ie/dww52sbt(v=vs.94).aspx + if (isError(value) + && (keys.indexOf('message') >= 0 || keys.indexOf('description') >= 0)) { + return formatError(value); + } + + // Some type of object without properties can be shortcutted. + if (keys.length === 0) { + if (isFunction(value)) { + var name = value.name ? ': ' + value.name : ''; + return ctx.stylize('[Function' + name + ']', 'special'); + } + if (isRegExp(value)) { + return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp'); + } + if (isDate(value)) { + return ctx.stylize(Date.prototype.toString.call(value), 'date'); + } + if (isError(value)) { + return formatError(value); + } + } + + var base = '', array = false, braces = ['{', '}']; + + // Make Array say that they are Array + if (isArray(value)) { + array = true; + braces = ['[', ']']; + } + + // Make functions say that they are functions + if (isFunction(value)) { + var n = value.name ? ': ' + value.name : ''; + base = ' [Function' + n + ']'; + } + + // Make RegExps say that they are RegExps + if (isRegExp(value)) { + base = ' ' + RegExp.prototype.toString.call(value); + } + + // Make dates with properties first say the date + if (isDate(value)) { + base = ' ' + Date.prototype.toUTCString.call(value); + } + + // Make error with message first say the error + if (isError(value)) { + base = ' ' + formatError(value); + } + + if (keys.length === 0 && (!array || value.length == 0)) { + return braces[0] + base + braces[1]; + } + + if (recurseTimes < 0) { + if (isRegExp(value)) { + return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp'); + } else { + return ctx.stylize('[Object]', 'special'); + } + } + + ctx.seen.push(value); + + var output; + if (array) { + output = formatArray(ctx, value, recurseTimes, visibleKeys, keys); + } else { + output = keys.map(function(key) { + return formatProperty(ctx, value, recurseTimes, visibleKeys, key, array); + }); + } + + ctx.seen.pop(); + + return reduceToSingleString(output, base, braces); +} + + +function formatPrimitive(ctx, value) { + if (isUndefined(value)) + return ctx.stylize('undefined', 'undefined'); + if (isString(value)) { + var simple = '\'' + JSON.stringify(value).replace(/^"|"$/g, '') + .replace(/'/g, "\\'") + .replace(/\\"/g, '"') + '\''; + return ctx.stylize(simple, 'string'); + } + if (isNumber(value)) + return ctx.stylize('' + value, 'number'); + if (isBoolean(value)) + return ctx.stylize('' + value, 'boolean'); + // For some reason typeof null is "object", so special case here. + if (isNull(value)) + return ctx.stylize('null', 'null'); +} + + +function formatError(value) { + return '[' + Error.prototype.toString.call(value) + ']'; +} + + +function formatArray(ctx, value, recurseTimes, visibleKeys, keys) { + var output = []; + for (var i = 0, l = value.length; i < l; ++i) { + if (hasOwnProperty(value, String(i))) { + output.push(formatProperty(ctx, value, recurseTimes, visibleKeys, + String(i), true)); + } else { + output.push(''); + } + } + keys.forEach(function(key) { + if (!key.match(/^\d+$/)) { + output.push(formatProperty(ctx, value, recurseTimes, visibleKeys, + key, true)); + } + }); + return output; +} + + +function formatProperty(ctx, value, recurseTimes, visibleKeys, key, array) { + var name, str, desc; + desc = Object.getOwnPropertyDescriptor(value, key) || { value: value[key] }; + if (desc.get) { + if (desc.set) { + str = ctx.stylize('[Getter/Setter]', 'special'); + } else { + str = ctx.stylize('[Getter]', 'special'); + } + } else { + if (desc.set) { + str = ctx.stylize('[Setter]', 'special'); + } + } + if (!hasOwnProperty(visibleKeys, key)) { + name = '[' + key + ']'; + } + if (!str) { + if (ctx.seen.indexOf(desc.value) < 0) { + if (isNull(recurseTimes)) { + str = formatValue(ctx, desc.value, null); + } else { + str = formatValue(ctx, desc.value, recurseTimes - 1); + } + if (str.indexOf('\n') > -1) { + if (array) { + str = str.split('\n').map(function(line) { + return ' ' + line; + }).join('\n').substr(2); + } else { + str = '\n' + str.split('\n').map(function(line) { + return ' ' + line; + }).join('\n'); + } + } + } else { + str = ctx.stylize('[Circular]', 'special'); + } + } + if (isUndefined(name)) { + if (array && key.match(/^\d+$/)) { + return str; + } + name = JSON.stringify('' + key); + if (name.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)) { + name = name.substr(1, name.length - 2); + name = ctx.stylize(name, 'name'); + } else { + name = name.replace(/'/g, "\\'") + .replace(/\\"/g, '"') + .replace(/(^"|"$)/g, "'"); + name = ctx.stylize(name, 'string'); + } + } + + return name + ': ' + str; +} + + +function reduceToSingleString(output, base, braces) { + var numLinesEst = 0; + var length = output.reduce(function(prev, cur) { + numLinesEst++; + if (cur.indexOf('\n') >= 0) numLinesEst++; + return prev + cur.replace(/\u001b\[\d\d?m/g, '').length + 1; + }, 0); + + if (length > 60) { + return braces[0] + + (base === '' ? '' : base + '\n ') + + ' ' + + output.join(',\n ') + + ' ' + + braces[1]; + } + + return braces[0] + base + ' ' + output.join(', ') + ' ' + braces[1]; +} + + +// NOTE: These type checking functions intentionally don't use `instanceof` +// because it is fragile and can be easily faked with `Object.create()`. +function isArray(ar) { + return Array.isArray(ar); +} +exports.isArray = isArray; + +function isBoolean(arg) { + return typeof arg === 'boolean'; +} +exports.isBoolean = isBoolean; + +function isNull(arg) { + return arg === null; +} +exports.isNull = isNull; + +function isNullOrUndefined(arg) { + return arg == null; +} +exports.isNullOrUndefined = isNullOrUndefined; + +function isNumber(arg) { + return typeof arg === 'number'; +} +exports.isNumber = isNumber; + +function isString(arg) { + return typeof arg === 'string'; +} +exports.isString = isString; + +function isSymbol(arg) { + return typeof arg === 'symbol'; +} +exports.isSymbol = isSymbol; + +function isUndefined(arg) { + return arg === void 0; +} +exports.isUndefined = isUndefined; + +function isRegExp(re) { + return isObject(re) && objectToString(re) === '[object RegExp]'; +} +exports.isRegExp = isRegExp; + +function isObject(arg) { + return typeof arg === 'object' && arg !== null; +} +exports.isObject = isObject; + +function isDate(d) { + return isObject(d) && objectToString(d) === '[object Date]'; +} +exports.isDate = isDate; + +function isError(e) { + return isObject(e) && + (objectToString(e) === '[object Error]' || e instanceof Error); +} +exports.isError = isError; + +function isFunction(arg) { + return typeof arg === 'function'; +} +exports.isFunction = isFunction; + +function isPrimitive(arg) { + return arg === null || + typeof arg === 'boolean' || + typeof arg === 'number' || + typeof arg === 'string' || + typeof arg === 'symbol' || // ES6 symbol + typeof arg === 'undefined'; +} +exports.isPrimitive = isPrimitive; + +exports.isBuffer = require('./support/isBuffer'); + +function objectToString(o) { + return Object.prototype.toString.call(o); +} + + +function pad(n) { + return n < 10 ? '0' + n.toString(10) : n.toString(10); +} + + +var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', + 'Oct', 'Nov', 'Dec']; + +// 26 Feb 16:19:34 +function timestamp() { + var d = new Date(); + var time = [pad(d.getHours()), + pad(d.getMinutes()), + pad(d.getSeconds())].join(':'); + return [d.getDate(), months[d.getMonth()], time].join(' '); +} + + +// log is just a thin wrapper to console.log that prepends a timestamp +exports.log = function() { + console.log('%s - %s', timestamp(), exports.format.apply(exports, arguments)); +}; + + +/** + * Inherit the prototype methods from one constructor into another. + * + * The Function.prototype.inherits from lang.js rewritten as a standalone + * function (not on Function.prototype). NOTE: If this file is to be loaded + * during bootstrapping this function needs to be rewritten using some native + * functions as prototype setup using normal JavaScript does not work as + * expected during bootstrapping (see mirror.js in r114903). + * + * @param {function} ctor Constructor function which needs to inherit the + * prototype. + * @param {function} superCtor Constructor function to inherit prototype from. + */ +exports.inherits = require('inherits'); + +exports._extend = function(origin, add) { + // Don't do anything if add isn't an object + if (!add || !isObject(add)) return origin; + + var keys = Object.keys(add); + var i = keys.length; + while (i--) { + origin[keys[i]] = add[keys[i]]; + } + return origin; +}; + +function hasOwnProperty(obj, prop) { + return Object.prototype.hasOwnProperty.call(obj, prop); +} + +}).call(this,require('_process'),typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{"./support/isBuffer":27,"_process":24,"inherits":26}],29:[function(require,module,exports){ +// Returns a wrapper function that returns a wrapped callback +// The wrapper function should do some stuff, and return a +// presumably different callback function. +// This makes sure that own properties are retained, so that +// decorations and such are not lost along the way. +module.exports = wrappy +function wrappy (fn, cb) { + if (fn && cb) return wrappy(fn)(cb) + + if (typeof fn !== 'function') + throw new TypeError('need wrapper function') + + Object.keys(fn).forEach(function (k) { + wrapper[k] = fn[k] + }) + + return wrapper + + function wrapper() { + var args = new Array(arguments.length) + for (var i = 0; i < args.length; i++) { + args[i] = arguments[i] + } + var ret = fn.apply(this, args) + var cb = args[args.length-1] + if (typeof ret === 'function' && ret !== cb) { + Object.keys(cb).forEach(function (k) { + ret[k] = cb[k] + }) + } + return ret + } +} + +},{}]},{},[7])(7) +}); \ No newline at end of file diff --git a/v0.13.6/assets/javascripts/workers/search.5e67fbfe.min.js b/v0.13.6/assets/javascripts/workers/search.5e67fbfe.min.js new file mode 100644 index 0000000000..07b7f39bf8 --- /dev/null +++ b/v0.13.6/assets/javascripts/workers/search.5e67fbfe.min.js @@ -0,0 +1,48 @@ +(()=>{var ge=Object.create;var U=Object.defineProperty,ye=Object.defineProperties,me=Object.getOwnPropertyDescriptor,ve=Object.getOwnPropertyDescriptors,xe=Object.getOwnPropertyNames,G=Object.getOwnPropertySymbols,Se=Object.getPrototypeOf,X=Object.prototype.hasOwnProperty,Qe=Object.prototype.propertyIsEnumerable;var J=(t,e,r)=>e in t?U(t,e,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[e]=r,M=(t,e)=>{for(var r in e||(e={}))X.call(e,r)&&J(t,r,e[r]);if(G)for(var r of G(e))Qe.call(e,r)&&J(t,r,e[r]);return t},Z=(t,e)=>ye(t,ve(e));var K=(t,e)=>()=>(e||t((e={exports:{}}).exports,e),e.exports);var be=(t,e,r,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of xe(e))!X.call(t,i)&&i!==r&&U(t,i,{get:()=>e[i],enumerable:!(n=me(e,i))||n.enumerable});return t};var W=(t,e,r)=>(r=t!=null?ge(Se(t)):{},be(e||!t||!t.__esModule?U(r,"default",{value:t,enumerable:!0}):r,t));var z=(t,e,r)=>new Promise((n,i)=>{var s=u=>{try{a(r.next(u))}catch(c){i(c)}},o=u=>{try{a(r.throw(u))}catch(c){i(c)}},a=u=>u.done?n(u.value):Promise.resolve(u.value).then(s,o);a((r=r.apply(t,e)).next())});var re=K((ee,te)=>{/** + * lunr - http://lunrjs.com - A bit like Solr, but much smaller and not as bright - 2.3.9 + * Copyright (C) 2020 Oliver Nightingale + * @license MIT + */(function(){var t=function(e){var r=new t.Builder;return r.pipeline.add(t.trimmer,t.stopWordFilter,t.stemmer),r.searchPipeline.add(t.stemmer),e.call(r,r),r.build()};t.version="2.3.9";/*! + * lunr.utils + * Copyright (C) 2020 Oliver Nightingale + */t.utils={},t.utils.warn=function(e){return function(r){e.console&&console.warn&&console.warn(r)}}(this),t.utils.asString=function(e){return e==null?"":e.toString()},t.utils.clone=function(e){if(e==null)return e;for(var r=Object.create(null),n=Object.keys(e),i=0;i0){var h=t.utils.clone(r)||{};h.position=[a,c],h.index=s.length,s.push(new t.Token(n.slice(a,o),h))}a=o+1}}return s},t.tokenizer.separator=/[\s\-]+/;/*! + * lunr.Pipeline + * Copyright (C) 2020 Oliver Nightingale + */t.Pipeline=function(){this._stack=[]},t.Pipeline.registeredFunctions=Object.create(null),t.Pipeline.registerFunction=function(e,r){r in this.registeredFunctions&&t.utils.warn("Overwriting existing registered function: "+r),e.label=r,t.Pipeline.registeredFunctions[e.label]=e},t.Pipeline.warnIfFunctionNotRegistered=function(e){var r=e.label&&e.label in this.registeredFunctions;r||t.utils.warn(`Function is not registered with pipeline. This may cause problems when serialising the index. +`,e)},t.Pipeline.load=function(e){var r=new t.Pipeline;return e.forEach(function(n){var i=t.Pipeline.registeredFunctions[n];if(i)r.add(i);else throw new Error("Cannot load unregistered function: "+n)}),r},t.Pipeline.prototype.add=function(){var e=Array.prototype.slice.call(arguments);e.forEach(function(r){t.Pipeline.warnIfFunctionNotRegistered(r),this._stack.push(r)},this)},t.Pipeline.prototype.after=function(e,r){t.Pipeline.warnIfFunctionNotRegistered(r);var n=this._stack.indexOf(e);if(n==-1)throw new Error("Cannot find existingFn");n=n+1,this._stack.splice(n,0,r)},t.Pipeline.prototype.before=function(e,r){t.Pipeline.warnIfFunctionNotRegistered(r);var n=this._stack.indexOf(e);if(n==-1)throw new Error("Cannot find existingFn");this._stack.splice(n,0,r)},t.Pipeline.prototype.remove=function(e){var r=this._stack.indexOf(e);r!=-1&&this._stack.splice(r,1)},t.Pipeline.prototype.run=function(e){for(var r=this._stack.length,n=0;n1&&(oe&&(n=s),o!=e);)i=n-r,s=r+Math.floor(i/2),o=this.elements[s*2];if(o==e||o>e)return s*2;if(ou?h+=2:a==u&&(r+=n[c+1]*i[h+1],c+=2,h+=2);return r},t.Vector.prototype.similarity=function(e){return this.dot(e)/this.magnitude()||0},t.Vector.prototype.toArray=function(){for(var e=new Array(this.elements.length/2),r=1,n=0;r0){var o=s.str.charAt(0),a;o in s.node.edges?a=s.node.edges[o]:(a=new t.TokenSet,s.node.edges[o]=a),s.str.length==1&&(a.final=!0),i.push({node:a,editsRemaining:s.editsRemaining,str:s.str.slice(1)})}if(s.editsRemaining!=0){if("*"in s.node.edges)var u=s.node.edges["*"];else{var u=new t.TokenSet;s.node.edges["*"]=u}if(s.str.length==0&&(u.final=!0),i.push({node:u,editsRemaining:s.editsRemaining-1,str:s.str}),s.str.length>1&&i.push({node:s.node,editsRemaining:s.editsRemaining-1,str:s.str.slice(1)}),s.str.length==1&&(s.node.final=!0),s.str.length>=1){if("*"in s.node.edges)var c=s.node.edges["*"];else{var c=new t.TokenSet;s.node.edges["*"]=c}s.str.length==1&&(c.final=!0),i.push({node:c,editsRemaining:s.editsRemaining-1,str:s.str.slice(1)})}if(s.str.length>1){var h=s.str.charAt(0),y=s.str.charAt(1),g;y in s.node.edges?g=s.node.edges[y]:(g=new t.TokenSet,s.node.edges[y]=g),s.str.length==1&&(g.final=!0),i.push({node:g,editsRemaining:s.editsRemaining-1,str:h+s.str.slice(2)})}}}return n},t.TokenSet.fromString=function(e){for(var r=new t.TokenSet,n=r,i=0,s=e.length;i=e;r--){var n=this.uncheckedNodes[r],i=n.child.toString();i in this.minimizedNodes?n.parent.edges[n.char]=this.minimizedNodes[i]:(n.child._str=i,this.minimizedNodes[i]=n.child),this.uncheckedNodes.pop()}};/*! + * lunr.Index + * Copyright (C) 2020 Oliver Nightingale + */t.Index=function(e){this.invertedIndex=e.invertedIndex,this.fieldVectors=e.fieldVectors,this.tokenSet=e.tokenSet,this.fields=e.fields,this.pipeline=e.pipeline},t.Index.prototype.search=function(e){return this.query(function(r){var n=new t.QueryParser(e,r);n.parse()})},t.Index.prototype.query=function(e){for(var r=new t.Query(this.fields),n=Object.create(null),i=Object.create(null),s=Object.create(null),o=Object.create(null),a=Object.create(null),u=0;u1?this._b=1:this._b=e},t.Builder.prototype.k1=function(e){this._k1=e},t.Builder.prototype.add=function(e,r){var n=e[this._ref],i=Object.keys(this._fields);this._documents[n]=r||{},this.documentCount+=1;for(var s=0;s=this.length)return t.QueryLexer.EOS;var e=this.str.charAt(this.pos);return this.pos+=1,e},t.QueryLexer.prototype.width=function(){return this.pos-this.start},t.QueryLexer.prototype.ignore=function(){this.start==this.pos&&(this.pos+=1),this.start=this.pos},t.QueryLexer.prototype.backup=function(){this.pos-=1},t.QueryLexer.prototype.acceptDigitRun=function(){var e,r;do e=this.next(),r=e.charCodeAt(0);while(r>47&&r<58);e!=t.QueryLexer.EOS&&this.backup()},t.QueryLexer.prototype.more=function(){return this.pos1&&(e.backup(),e.emit(t.QueryLexer.TERM)),e.ignore(),e.more())return t.QueryLexer.lexText},t.QueryLexer.lexEditDistance=function(e){return e.ignore(),e.acceptDigitRun(),e.emit(t.QueryLexer.EDIT_DISTANCE),t.QueryLexer.lexText},t.QueryLexer.lexBoost=function(e){return e.ignore(),e.acceptDigitRun(),e.emit(t.QueryLexer.BOOST),t.QueryLexer.lexText},t.QueryLexer.lexEOS=function(e){e.width()>0&&e.emit(t.QueryLexer.TERM)},t.QueryLexer.termSeparator=t.tokenizer.separator,t.QueryLexer.lexText=function(e){for(;;){var r=e.next();if(r==t.QueryLexer.EOS)return t.QueryLexer.lexEOS;if(r.charCodeAt(0)==92){e.escapeCharacter();continue}if(r==":")return t.QueryLexer.lexField;if(r=="~")return e.backup(),e.width()>0&&e.emit(t.QueryLexer.TERM),t.QueryLexer.lexEditDistance;if(r=="^")return e.backup(),e.width()>0&&e.emit(t.QueryLexer.TERM),t.QueryLexer.lexBoost;if(r=="+"&&e.width()===1||r=="-"&&e.width()===1)return e.emit(t.QueryLexer.PRESENCE),t.QueryLexer.lexText;if(r.match(t.QueryLexer.termSeparator))return t.QueryLexer.lexTerm}},t.QueryParser=function(e,r){this.lexer=new t.QueryLexer(e),this.query=r,this.currentClause={},this.lexemeIdx=0},t.QueryParser.prototype.parse=function(){this.lexer.run(),this.lexemes=this.lexer.lexemes;for(var e=t.QueryParser.parseClause;e;)e=e(this);return this.query},t.QueryParser.prototype.peekLexeme=function(){return this.lexemes[this.lexemeIdx]},t.QueryParser.prototype.consumeLexeme=function(){var e=this.peekLexeme();return this.lexemeIdx+=1,e},t.QueryParser.prototype.nextClause=function(){var e=this.currentClause;this.query.clause(e),this.currentClause={}},t.QueryParser.parseClause=function(e){var r=e.peekLexeme();if(r!=null)switch(r.type){case t.QueryLexer.PRESENCE:return t.QueryParser.parsePresence;case t.QueryLexer.FIELD:return t.QueryParser.parseField;case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var n="expected either a field or a term, found "+r.type;throw r.str.length>=1&&(n+=" with value '"+r.str+"'"),new t.QueryParseError(n,r.start,r.end)}},t.QueryParser.parsePresence=function(e){var r=e.consumeLexeme();if(r!=null){switch(r.str){case"-":e.currentClause.presence=t.Query.presence.PROHIBITED;break;case"+":e.currentClause.presence=t.Query.presence.REQUIRED;break;default:var n="unrecognised presence operator'"+r.str+"'";throw new t.QueryParseError(n,r.start,r.end)}var i=e.peekLexeme();if(i==null){var n="expecting term or field, found nothing";throw new t.QueryParseError(n,r.start,r.end)}switch(i.type){case t.QueryLexer.FIELD:return t.QueryParser.parseField;case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var n="expecting term or field, found '"+i.type+"'";throw new t.QueryParseError(n,i.start,i.end)}}},t.QueryParser.parseField=function(e){var r=e.consumeLexeme();if(r!=null){if(e.query.allFields.indexOf(r.str)==-1){var n=e.query.allFields.map(function(o){return"'"+o+"'"}).join(", "),i="unrecognised field '"+r.str+"', possible fields: "+n;throw new t.QueryParseError(i,r.start,r.end)}e.currentClause.fields=[r.str];var s=e.peekLexeme();if(s==null){var i="expecting term, found nothing";throw new t.QueryParseError(i,r.start,r.end)}switch(s.type){case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var i="expecting term, found '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},t.QueryParser.parseTerm=function(e){var r=e.consumeLexeme();if(r!=null){e.currentClause.term=r.str.toLowerCase(),r.str.indexOf("*")!=-1&&(e.currentClause.usePipeline=!1);var n=e.peekLexeme();if(n==null){e.nextClause();return}switch(n.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+n.type+"'";throw new t.QueryParseError(i,n.start,n.end)}}},t.QueryParser.parseEditDistance=function(e){var r=e.consumeLexeme();if(r!=null){var n=parseInt(r.str,10);if(isNaN(n)){var i="edit distance must be numeric";throw new t.QueryParseError(i,r.start,r.end)}e.currentClause.editDistance=n;var s=e.peekLexeme();if(s==null){e.nextClause();return}switch(s.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},t.QueryParser.parseBoost=function(e){var r=e.consumeLexeme();if(r!=null){var n=parseInt(r.str,10);if(isNaN(n)){var i="boost must be numeric";throw new t.QueryParseError(i,r.start,r.end)}e.currentClause.boost=n;var s=e.peekLexeme();if(s==null){e.nextClause();return}switch(s.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},function(e,r){typeof define=="function"&&define.amd?define(r):typeof ee=="object"?te.exports=r():e.lunr=r()}(this,function(){return t})})()});var H=K((Re,ne)=>{"use strict";/*! + * escape-html + * Copyright(c) 2012-2013 TJ Holowaychuk + * Copyright(c) 2015 Andreas Lubbe + * Copyright(c) 2015 Tiancheng "Timothy" Gu + * MIT Licensed + */var Le=/["'&<>]/;ne.exports=we;function we(t){var e=""+t,r=Le.exec(e);if(!r)return e;var n,i="",s=0,o=0;for(s=r.index;s=0;r--){let n=t[r];typeof n!="object"?n=document.createTextNode(n):n.parentNode&&n.parentNode.removeChild(n),r?e.insertBefore(this.previousSibling,n):e.replaceChild(n,this)}}}));var ie=W(H());function se(t){let e=new Map,r=new Set;for(let n of t){let[i,s]=n.location.split("#"),o=n.location,a=n.title,u=n.tags,c=(0,ie.default)(n.text).replace(/\s+(?=[,.:;!?])/g,"").replace(/\s+/g," ");if(s){let h=e.get(i);r.has(h)?e.set(o,{location:o,title:a,text:c,parent:h}):(h.title=n.title,h.text=c,r.add(h))}else e.set(o,M({location:o,title:a,text:c},u&&{tags:u}))}return e}var oe=W(H());function ae(t,e){let r=new RegExp(t.separator,"img"),n=(i,s,o)=>`${s}${o}`;return i=>{i=i.replace(/[\s*+\-:~^]+/g," ").trim();let s=new RegExp(`(^|${t.separator})(${i.replace(/[|\\{}()[\]^$+*?.-]/g,"\\$&").replace(r,"|")})`,"img");return o=>(e?(0,oe.default)(o):o).replace(s,n).replace(/<\/mark>(\s+)]*>/img,"$1")}}function ue(t){let e=new lunr.Query(["title","text"]);return new lunr.QueryParser(t,e).parse(),e.clauses}function ce(t,e){var i;let r=new Set(t),n={};for(let s=0;s!n.has(i)))]}var q=class{constructor({config:e,docs:r,options:n}){this.options=n,this.documents=se(r),this.highlight=ae(e,!1),lunr.tokenizer.separator=new RegExp(e.separator),this.index=lunr(function(){e.lang.length===1&&e.lang[0]!=="en"?this.use(lunr[e.lang[0]]):e.lang.length>1&&this.use(lunr.multiLanguage(...e.lang));let i=Ee(["trimmer","stopWordFilter","stemmer"],n.pipeline);for(let s of e.lang.map(o=>o==="en"?lunr:lunr[o]))for(let o of i)this.pipeline.remove(s[o]),this.searchPipeline.remove(s[o]);this.ref("location"),this.field("title",{boost:1e3}),this.field("text"),this.field("tags",{boost:1e6});for(let s of r)this.add(s)})}search(e){if(e)try{let r=this.highlight(e),n=ue(e).filter(o=>o.presence!==lunr.Query.presence.PROHIBITED),i=this.index.search(`${e}*`).reduce((o,{ref:a,score:u,matchData:c})=>{let h=this.documents.get(a);if(typeof h!="undefined"){let{location:y,title:g,text:b,tags:m,parent:Q}=h,p=ce(n,Object.keys(c.metadata)),d=+!Q+ +Object.values(p).every(w=>w);o.push(Z(M({location:y,title:r(g),text:r(b)},m&&{tags:m.map(r)}),{score:u*(1+d),terms:p}))}return o},[]).sort((o,a)=>a.score-o.score).reduce((o,a)=>{let u=this.documents.get(a.location);if(typeof u!="undefined"){let c="parent"in u?u.parent.location:u.location;o.set(c,[...o.get(c)||[],a])}return o},new Map),s;if(this.options.suggestions){let o=this.index.query(a=>{for(let u of n)a.term(u.term,{fields:["title"],presence:lunr.Query.presence.REQUIRED,wildcard:lunr.Query.wildcard.TRAILING})});s=o.length?Object.keys(o[0].matchData.metadata):[]}return M({items:[...i.values()]},typeof s!="undefined"&&{suggestions:s})}catch(r){console.warn(`Invalid query: ${e} \u2013 see https://bit.ly/2s3ChXG`)}return{items:[]}}};var Y;function ke(t){return z(this,null,function*(){let e="../lunr";if(typeof parent!="undefined"&&"IFrameWorker"in parent){let n=document.querySelector("script[src]"),[i]=n.src.split("/worker");e=e.replace("..",i)}let r=[];for(let n of t.lang){switch(n){case"ja":r.push(`${e}/tinyseg.js`);break;case"hi":case"th":r.push(`${e}/wordcut.js`);break}n!=="en"&&r.push(`${e}/min/lunr.${n}.min.js`)}t.lang.length>1&&r.push(`${e}/min/lunr.multi.min.js`),r.length&&(yield importScripts(`${e}/min/lunr.stemmer.support.min.js`,...r))})}function Te(t){return z(this,null,function*(){switch(t.type){case 0:return yield ke(t.data.config),Y=new q(t.data),{type:1};case 2:return{type:3,data:Y?Y.search(t.data):{items:[]}};default:throw new TypeError("Invalid message type")}})}self.lunr=le.default;addEventListener("message",t=>z(void 0,null,function*(){postMessage(yield Te(t.data))}));})(); +//# sourceMappingURL=search.5e67fbfe.min.js.map + diff --git a/v0.13.6/assets/javascripts/workers/search.5e67fbfe.min.js.map b/v0.13.6/assets/javascripts/workers/search.5e67fbfe.min.js.map new file mode 100644 index 0000000000..06d43304ec --- /dev/null +++ b/v0.13.6/assets/javascripts/workers/search.5e67fbfe.min.js.map @@ -0,0 +1,8 @@ +{ + "version": 3, + "sources": ["node_modules/lunr/lunr.js", "node_modules/escape-html/index.js", "src/assets/javascripts/integrations/search/worker/main/index.ts", "src/assets/javascripts/polyfills/index.ts", "src/assets/javascripts/integrations/search/document/index.ts", "src/assets/javascripts/integrations/search/highlighter/index.ts", "src/assets/javascripts/integrations/search/query/_/index.ts", "src/assets/javascripts/integrations/search/_/index.ts"], + "sourceRoot": "../../../..", + "sourcesContent": ["/**\n * lunr - http://lunrjs.com - A bit like Solr, but much smaller and not as bright - 2.3.9\n * Copyright (C) 2020 Oliver Nightingale\n * @license MIT\n */\n\n;(function(){\n\n/**\n * A convenience function for configuring and constructing\n * a new lunr Index.\n *\n * A lunr.Builder instance is created and the pipeline setup\n * with a trimmer, stop word filter and stemmer.\n *\n * This builder object is yielded to the configuration function\n * that is passed as a parameter, allowing the list of fields\n * and other builder parameters to be customised.\n *\n * All documents _must_ be added within the passed config function.\n *\n * @example\n * var idx = lunr(function () {\n * this.field('title')\n * this.field('body')\n * this.ref('id')\n *\n * documents.forEach(function (doc) {\n * this.add(doc)\n * }, this)\n * })\n *\n * @see {@link lunr.Builder}\n * @see {@link lunr.Pipeline}\n * @see {@link lunr.trimmer}\n * @see {@link lunr.stopWordFilter}\n * @see {@link lunr.stemmer}\n * @namespace {function} lunr\n */\nvar lunr = function (config) {\n var builder = new lunr.Builder\n\n builder.pipeline.add(\n lunr.trimmer,\n lunr.stopWordFilter,\n lunr.stemmer\n )\n\n builder.searchPipeline.add(\n lunr.stemmer\n )\n\n config.call(builder, builder)\n return builder.build()\n}\n\nlunr.version = \"2.3.9\"\n/*!\n * lunr.utils\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * A namespace containing utils for the rest of the lunr library\n * @namespace lunr.utils\n */\nlunr.utils = {}\n\n/**\n * Print a warning message to the console.\n *\n * @param {String} message The message to be printed.\n * @memberOf lunr.utils\n * @function\n */\nlunr.utils.warn = (function (global) {\n /* eslint-disable no-console */\n return function (message) {\n if (global.console && console.warn) {\n console.warn(message)\n }\n }\n /* eslint-enable no-console */\n})(this)\n\n/**\n * Convert an object to a string.\n *\n * In the case of `null` and `undefined` the function returns\n * the empty string, in all other cases the result of calling\n * `toString` on the passed object is returned.\n *\n * @param {Any} obj The object to convert to a string.\n * @return {String} string representation of the passed object.\n * @memberOf lunr.utils\n */\nlunr.utils.asString = function (obj) {\n if (obj === void 0 || obj === null) {\n return \"\"\n } else {\n return obj.toString()\n }\n}\n\n/**\n * Clones an object.\n *\n * Will create a copy of an existing object such that any mutations\n * on the copy cannot affect the original.\n *\n * Only shallow objects are supported, passing a nested object to this\n * function will cause a TypeError.\n *\n * Objects with primitives, and arrays of primitives are supported.\n *\n * @param {Object} obj The object to clone.\n * @return {Object} a clone of the passed object.\n * @throws {TypeError} when a nested object is passed.\n * @memberOf Utils\n */\nlunr.utils.clone = function (obj) {\n if (obj === null || obj === undefined) {\n return obj\n }\n\n var clone = Object.create(null),\n keys = Object.keys(obj)\n\n for (var i = 0; i < keys.length; i++) {\n var key = keys[i],\n val = obj[key]\n\n if (Array.isArray(val)) {\n clone[key] = val.slice()\n continue\n }\n\n if (typeof val === 'string' ||\n typeof val === 'number' ||\n typeof val === 'boolean') {\n clone[key] = val\n continue\n }\n\n throw new TypeError(\"clone is not deep and does not support nested objects\")\n }\n\n return clone\n}\nlunr.FieldRef = function (docRef, fieldName, stringValue) {\n this.docRef = docRef\n this.fieldName = fieldName\n this._stringValue = stringValue\n}\n\nlunr.FieldRef.joiner = \"/\"\n\nlunr.FieldRef.fromString = function (s) {\n var n = s.indexOf(lunr.FieldRef.joiner)\n\n if (n === -1) {\n throw \"malformed field ref string\"\n }\n\n var fieldRef = s.slice(0, n),\n docRef = s.slice(n + 1)\n\n return new lunr.FieldRef (docRef, fieldRef, s)\n}\n\nlunr.FieldRef.prototype.toString = function () {\n if (this._stringValue == undefined) {\n this._stringValue = this.fieldName + lunr.FieldRef.joiner + this.docRef\n }\n\n return this._stringValue\n}\n/*!\n * lunr.Set\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * A lunr set.\n *\n * @constructor\n */\nlunr.Set = function (elements) {\n this.elements = Object.create(null)\n\n if (elements) {\n this.length = elements.length\n\n for (var i = 0; i < this.length; i++) {\n this.elements[elements[i]] = true\n }\n } else {\n this.length = 0\n }\n}\n\n/**\n * A complete set that contains all elements.\n *\n * @static\n * @readonly\n * @type {lunr.Set}\n */\nlunr.Set.complete = {\n intersect: function (other) {\n return other\n },\n\n union: function () {\n return this\n },\n\n contains: function () {\n return true\n }\n}\n\n/**\n * An empty set that contains no elements.\n *\n * @static\n * @readonly\n * @type {lunr.Set}\n */\nlunr.Set.empty = {\n intersect: function () {\n return this\n },\n\n union: function (other) {\n return other\n },\n\n contains: function () {\n return false\n }\n}\n\n/**\n * Returns true if this set contains the specified object.\n *\n * @param {object} object - Object whose presence in this set is to be tested.\n * @returns {boolean} - True if this set contains the specified object.\n */\nlunr.Set.prototype.contains = function (object) {\n return !!this.elements[object]\n}\n\n/**\n * Returns a new set containing only the elements that are present in both\n * this set and the specified set.\n *\n * @param {lunr.Set} other - set to intersect with this set.\n * @returns {lunr.Set} a new set that is the intersection of this and the specified set.\n */\n\nlunr.Set.prototype.intersect = function (other) {\n var a, b, elements, intersection = []\n\n if (other === lunr.Set.complete) {\n return this\n }\n\n if (other === lunr.Set.empty) {\n return other\n }\n\n if (this.length < other.length) {\n a = this\n b = other\n } else {\n a = other\n b = this\n }\n\n elements = Object.keys(a.elements)\n\n for (var i = 0; i < elements.length; i++) {\n var element = elements[i]\n if (element in b.elements) {\n intersection.push(element)\n }\n }\n\n return new lunr.Set (intersection)\n}\n\n/**\n * Returns a new set combining the elements of this and the specified set.\n *\n * @param {lunr.Set} other - set to union with this set.\n * @return {lunr.Set} a new set that is the union of this and the specified set.\n */\n\nlunr.Set.prototype.union = function (other) {\n if (other === lunr.Set.complete) {\n return lunr.Set.complete\n }\n\n if (other === lunr.Set.empty) {\n return this\n }\n\n return new lunr.Set(Object.keys(this.elements).concat(Object.keys(other.elements)))\n}\n/**\n * A function to calculate the inverse document frequency for\n * a posting. This is shared between the builder and the index\n *\n * @private\n * @param {object} posting - The posting for a given term\n * @param {number} documentCount - The total number of documents.\n */\nlunr.idf = function (posting, documentCount) {\n var documentsWithTerm = 0\n\n for (var fieldName in posting) {\n if (fieldName == '_index') continue // Ignore the term index, its not a field\n documentsWithTerm += Object.keys(posting[fieldName]).length\n }\n\n var x = (documentCount - documentsWithTerm + 0.5) / (documentsWithTerm + 0.5)\n\n return Math.log(1 + Math.abs(x))\n}\n\n/**\n * A token wraps a string representation of a token\n * as it is passed through the text processing pipeline.\n *\n * @constructor\n * @param {string} [str=''] - The string token being wrapped.\n * @param {object} [metadata={}] - Metadata associated with this token.\n */\nlunr.Token = function (str, metadata) {\n this.str = str || \"\"\n this.metadata = metadata || {}\n}\n\n/**\n * Returns the token string that is being wrapped by this object.\n *\n * @returns {string}\n */\nlunr.Token.prototype.toString = function () {\n return this.str\n}\n\n/**\n * A token update function is used when updating or optionally\n * when cloning a token.\n *\n * @callback lunr.Token~updateFunction\n * @param {string} str - The string representation of the token.\n * @param {Object} metadata - All metadata associated with this token.\n */\n\n/**\n * Applies the given function to the wrapped string token.\n *\n * @example\n * token.update(function (str, metadata) {\n * return str.toUpperCase()\n * })\n *\n * @param {lunr.Token~updateFunction} fn - A function to apply to the token string.\n * @returns {lunr.Token}\n */\nlunr.Token.prototype.update = function (fn) {\n this.str = fn(this.str, this.metadata)\n return this\n}\n\n/**\n * Creates a clone of this token. Optionally a function can be\n * applied to the cloned token.\n *\n * @param {lunr.Token~updateFunction} [fn] - An optional function to apply to the cloned token.\n * @returns {lunr.Token}\n */\nlunr.Token.prototype.clone = function (fn) {\n fn = fn || function (s) { return s }\n return new lunr.Token (fn(this.str, this.metadata), this.metadata)\n}\n/*!\n * lunr.tokenizer\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * A function for splitting a string into tokens ready to be inserted into\n * the search index. Uses `lunr.tokenizer.separator` to split strings, change\n * the value of this property to change how strings are split into tokens.\n *\n * This tokenizer will convert its parameter to a string by calling `toString` and\n * then will split this string on the character in `lunr.tokenizer.separator`.\n * Arrays will have their elements converted to strings and wrapped in a lunr.Token.\n *\n * Optional metadata can be passed to the tokenizer, this metadata will be cloned and\n * added as metadata to every token that is created from the object to be tokenized.\n *\n * @static\n * @param {?(string|object|object[])} obj - The object to convert into tokens\n * @param {?object} metadata - Optional metadata to associate with every token\n * @returns {lunr.Token[]}\n * @see {@link lunr.Pipeline}\n */\nlunr.tokenizer = function (obj, metadata) {\n if (obj == null || obj == undefined) {\n return []\n }\n\n if (Array.isArray(obj)) {\n return obj.map(function (t) {\n return new lunr.Token(\n lunr.utils.asString(t).toLowerCase(),\n lunr.utils.clone(metadata)\n )\n })\n }\n\n var str = obj.toString().toLowerCase(),\n len = str.length,\n tokens = []\n\n for (var sliceEnd = 0, sliceStart = 0; sliceEnd <= len; sliceEnd++) {\n var char = str.charAt(sliceEnd),\n sliceLength = sliceEnd - sliceStart\n\n if ((char.match(lunr.tokenizer.separator) || sliceEnd == len)) {\n\n if (sliceLength > 0) {\n var tokenMetadata = lunr.utils.clone(metadata) || {}\n tokenMetadata[\"position\"] = [sliceStart, sliceLength]\n tokenMetadata[\"index\"] = tokens.length\n\n tokens.push(\n new lunr.Token (\n str.slice(sliceStart, sliceEnd),\n tokenMetadata\n )\n )\n }\n\n sliceStart = sliceEnd + 1\n }\n\n }\n\n return tokens\n}\n\n/**\n * The separator used to split a string into tokens. Override this property to change the behaviour of\n * `lunr.tokenizer` behaviour when tokenizing strings. By default this splits on whitespace and hyphens.\n *\n * @static\n * @see lunr.tokenizer\n */\nlunr.tokenizer.separator = /[\\s\\-]+/\n/*!\n * lunr.Pipeline\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * lunr.Pipelines maintain an ordered list of functions to be applied to all\n * tokens in documents entering the search index and queries being ran against\n * the index.\n *\n * An instance of lunr.Index created with the lunr shortcut will contain a\n * pipeline with a stop word filter and an English language stemmer. Extra\n * functions can be added before or after either of these functions or these\n * default functions can be removed.\n *\n * When run the pipeline will call each function in turn, passing a token, the\n * index of that token in the original list of all tokens and finally a list of\n * all the original tokens.\n *\n * The output of functions in the pipeline will be passed to the next function\n * in the pipeline. To exclude a token from entering the index the function\n * should return undefined, the rest of the pipeline will not be called with\n * this token.\n *\n * For serialisation of pipelines to work, all functions used in an instance of\n * a pipeline should be registered with lunr.Pipeline. Registered functions can\n * then be loaded. If trying to load a serialised pipeline that uses functions\n * that are not registered an error will be thrown.\n *\n * If not planning on serialising the pipeline then registering pipeline functions\n * is not necessary.\n *\n * @constructor\n */\nlunr.Pipeline = function () {\n this._stack = []\n}\n\nlunr.Pipeline.registeredFunctions = Object.create(null)\n\n/**\n * A pipeline function maps lunr.Token to lunr.Token. A lunr.Token contains the token\n * string as well as all known metadata. A pipeline function can mutate the token string\n * or mutate (or add) metadata for a given token.\n *\n * A pipeline function can indicate that the passed token should be discarded by returning\n * null, undefined or an empty string. This token will not be passed to any downstream pipeline\n * functions and will not be added to the index.\n *\n * Multiple tokens can be returned by returning an array of tokens. Each token will be passed\n * to any downstream pipeline functions and all will returned tokens will be added to the index.\n *\n * Any number of pipeline functions may be chained together using a lunr.Pipeline.\n *\n * @interface lunr.PipelineFunction\n * @param {lunr.Token} token - A token from the document being processed.\n * @param {number} i - The index of this token in the complete list of tokens for this document/field.\n * @param {lunr.Token[]} tokens - All tokens for this document/field.\n * @returns {(?lunr.Token|lunr.Token[])}\n */\n\n/**\n * Register a function with the pipeline.\n *\n * Functions that are used in the pipeline should be registered if the pipeline\n * needs to be serialised, or a serialised pipeline needs to be loaded.\n *\n * Registering a function does not add it to a pipeline, functions must still be\n * added to instances of the pipeline for them to be used when running a pipeline.\n *\n * @param {lunr.PipelineFunction} fn - The function to check for.\n * @param {String} label - The label to register this function with\n */\nlunr.Pipeline.registerFunction = function (fn, label) {\n if (label in this.registeredFunctions) {\n lunr.utils.warn('Overwriting existing registered function: ' + label)\n }\n\n fn.label = label\n lunr.Pipeline.registeredFunctions[fn.label] = fn\n}\n\n/**\n * Warns if the function is not registered as a Pipeline function.\n *\n * @param {lunr.PipelineFunction} fn - The function to check for.\n * @private\n */\nlunr.Pipeline.warnIfFunctionNotRegistered = function (fn) {\n var isRegistered = fn.label && (fn.label in this.registeredFunctions)\n\n if (!isRegistered) {\n lunr.utils.warn('Function is not registered with pipeline. This may cause problems when serialising the index.\\n', fn)\n }\n}\n\n/**\n * Loads a previously serialised pipeline.\n *\n * All functions to be loaded must already be registered with lunr.Pipeline.\n * If any function from the serialised data has not been registered then an\n * error will be thrown.\n *\n * @param {Object} serialised - The serialised pipeline to load.\n * @returns {lunr.Pipeline}\n */\nlunr.Pipeline.load = function (serialised) {\n var pipeline = new lunr.Pipeline\n\n serialised.forEach(function (fnName) {\n var fn = lunr.Pipeline.registeredFunctions[fnName]\n\n if (fn) {\n pipeline.add(fn)\n } else {\n throw new Error('Cannot load unregistered function: ' + fnName)\n }\n })\n\n return pipeline\n}\n\n/**\n * Adds new functions to the end of the pipeline.\n *\n * Logs a warning if the function has not been registered.\n *\n * @param {lunr.PipelineFunction[]} functions - Any number of functions to add to the pipeline.\n */\nlunr.Pipeline.prototype.add = function () {\n var fns = Array.prototype.slice.call(arguments)\n\n fns.forEach(function (fn) {\n lunr.Pipeline.warnIfFunctionNotRegistered(fn)\n this._stack.push(fn)\n }, this)\n}\n\n/**\n * Adds a single function after a function that already exists in the\n * pipeline.\n *\n * Logs a warning if the function has not been registered.\n *\n * @param {lunr.PipelineFunction} existingFn - A function that already exists in the pipeline.\n * @param {lunr.PipelineFunction} newFn - The new function to add to the pipeline.\n */\nlunr.Pipeline.prototype.after = function (existingFn, newFn) {\n lunr.Pipeline.warnIfFunctionNotRegistered(newFn)\n\n var pos = this._stack.indexOf(existingFn)\n if (pos == -1) {\n throw new Error('Cannot find existingFn')\n }\n\n pos = pos + 1\n this._stack.splice(pos, 0, newFn)\n}\n\n/**\n * Adds a single function before a function that already exists in the\n * pipeline.\n *\n * Logs a warning if the function has not been registered.\n *\n * @param {lunr.PipelineFunction} existingFn - A function that already exists in the pipeline.\n * @param {lunr.PipelineFunction} newFn - The new function to add to the pipeline.\n */\nlunr.Pipeline.prototype.before = function (existingFn, newFn) {\n lunr.Pipeline.warnIfFunctionNotRegistered(newFn)\n\n var pos = this._stack.indexOf(existingFn)\n if (pos == -1) {\n throw new Error('Cannot find existingFn')\n }\n\n this._stack.splice(pos, 0, newFn)\n}\n\n/**\n * Removes a function from the pipeline.\n *\n * @param {lunr.PipelineFunction} fn The function to remove from the pipeline.\n */\nlunr.Pipeline.prototype.remove = function (fn) {\n var pos = this._stack.indexOf(fn)\n if (pos == -1) {\n return\n }\n\n this._stack.splice(pos, 1)\n}\n\n/**\n * Runs the current list of functions that make up the pipeline against the\n * passed tokens.\n *\n * @param {Array} tokens The tokens to run through the pipeline.\n * @returns {Array}\n */\nlunr.Pipeline.prototype.run = function (tokens) {\n var stackLength = this._stack.length\n\n for (var i = 0; i < stackLength; i++) {\n var fn = this._stack[i]\n var memo = []\n\n for (var j = 0; j < tokens.length; j++) {\n var result = fn(tokens[j], j, tokens)\n\n if (result === null || result === void 0 || result === '') continue\n\n if (Array.isArray(result)) {\n for (var k = 0; k < result.length; k++) {\n memo.push(result[k])\n }\n } else {\n memo.push(result)\n }\n }\n\n tokens = memo\n }\n\n return tokens\n}\n\n/**\n * Convenience method for passing a string through a pipeline and getting\n * strings out. This method takes care of wrapping the passed string in a\n * token and mapping the resulting tokens back to strings.\n *\n * @param {string} str - The string to pass through the pipeline.\n * @param {?object} metadata - Optional metadata to associate with the token\n * passed to the pipeline.\n * @returns {string[]}\n */\nlunr.Pipeline.prototype.runString = function (str, metadata) {\n var token = new lunr.Token (str, metadata)\n\n return this.run([token]).map(function (t) {\n return t.toString()\n })\n}\n\n/**\n * Resets the pipeline by removing any existing processors.\n *\n */\nlunr.Pipeline.prototype.reset = function () {\n this._stack = []\n}\n\n/**\n * Returns a representation of the pipeline ready for serialisation.\n *\n * Logs a warning if the function has not been registered.\n *\n * @returns {Array}\n */\nlunr.Pipeline.prototype.toJSON = function () {\n return this._stack.map(function (fn) {\n lunr.Pipeline.warnIfFunctionNotRegistered(fn)\n\n return fn.label\n })\n}\n/*!\n * lunr.Vector\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * A vector is used to construct the vector space of documents and queries. These\n * vectors support operations to determine the similarity between two documents or\n * a document and a query.\n *\n * Normally no parameters are required for initializing a vector, but in the case of\n * loading a previously dumped vector the raw elements can be provided to the constructor.\n *\n * For performance reasons vectors are implemented with a flat array, where an elements\n * index is immediately followed by its value. E.g. [index, value, index, value]. This\n * allows the underlying array to be as sparse as possible and still offer decent\n * performance when being used for vector calculations.\n *\n * @constructor\n * @param {Number[]} [elements] - The flat list of element index and element value pairs.\n */\nlunr.Vector = function (elements) {\n this._magnitude = 0\n this.elements = elements || []\n}\n\n\n/**\n * Calculates the position within the vector to insert a given index.\n *\n * This is used internally by insert and upsert. If there are duplicate indexes then\n * the position is returned as if the value for that index were to be updated, but it\n * is the callers responsibility to check whether there is a duplicate at that index\n *\n * @param {Number} insertIdx - The index at which the element should be inserted.\n * @returns {Number}\n */\nlunr.Vector.prototype.positionForIndex = function (index) {\n // For an empty vector the tuple can be inserted at the beginning\n if (this.elements.length == 0) {\n return 0\n }\n\n var start = 0,\n end = this.elements.length / 2,\n sliceLength = end - start,\n pivotPoint = Math.floor(sliceLength / 2),\n pivotIndex = this.elements[pivotPoint * 2]\n\n while (sliceLength > 1) {\n if (pivotIndex < index) {\n start = pivotPoint\n }\n\n if (pivotIndex > index) {\n end = pivotPoint\n }\n\n if (pivotIndex == index) {\n break\n }\n\n sliceLength = end - start\n pivotPoint = start + Math.floor(sliceLength / 2)\n pivotIndex = this.elements[pivotPoint * 2]\n }\n\n if (pivotIndex == index) {\n return pivotPoint * 2\n }\n\n if (pivotIndex > index) {\n return pivotPoint * 2\n }\n\n if (pivotIndex < index) {\n return (pivotPoint + 1) * 2\n }\n}\n\n/**\n * Inserts an element at an index within the vector.\n *\n * Does not allow duplicates, will throw an error if there is already an entry\n * for this index.\n *\n * @param {Number} insertIdx - The index at which the element should be inserted.\n * @param {Number} val - The value to be inserted into the vector.\n */\nlunr.Vector.prototype.insert = function (insertIdx, val) {\n this.upsert(insertIdx, val, function () {\n throw \"duplicate index\"\n })\n}\n\n/**\n * Inserts or updates an existing index within the vector.\n *\n * @param {Number} insertIdx - The index at which the element should be inserted.\n * @param {Number} val - The value to be inserted into the vector.\n * @param {function} fn - A function that is called for updates, the existing value and the\n * requested value are passed as arguments\n */\nlunr.Vector.prototype.upsert = function (insertIdx, val, fn) {\n this._magnitude = 0\n var position = this.positionForIndex(insertIdx)\n\n if (this.elements[position] == insertIdx) {\n this.elements[position + 1] = fn(this.elements[position + 1], val)\n } else {\n this.elements.splice(position, 0, insertIdx, val)\n }\n}\n\n/**\n * Calculates the magnitude of this vector.\n *\n * @returns {Number}\n */\nlunr.Vector.prototype.magnitude = function () {\n if (this._magnitude) return this._magnitude\n\n var sumOfSquares = 0,\n elementsLength = this.elements.length\n\n for (var i = 1; i < elementsLength; i += 2) {\n var val = this.elements[i]\n sumOfSquares += val * val\n }\n\n return this._magnitude = Math.sqrt(sumOfSquares)\n}\n\n/**\n * Calculates the dot product of this vector and another vector.\n *\n * @param {lunr.Vector} otherVector - The vector to compute the dot product with.\n * @returns {Number}\n */\nlunr.Vector.prototype.dot = function (otherVector) {\n var dotProduct = 0,\n a = this.elements, b = otherVector.elements,\n aLen = a.length, bLen = b.length,\n aVal = 0, bVal = 0,\n i = 0, j = 0\n\n while (i < aLen && j < bLen) {\n aVal = a[i], bVal = b[j]\n if (aVal < bVal) {\n i += 2\n } else if (aVal > bVal) {\n j += 2\n } else if (aVal == bVal) {\n dotProduct += a[i + 1] * b[j + 1]\n i += 2\n j += 2\n }\n }\n\n return dotProduct\n}\n\n/**\n * Calculates the similarity between this vector and another vector.\n *\n * @param {lunr.Vector} otherVector - The other vector to calculate the\n * similarity with.\n * @returns {Number}\n */\nlunr.Vector.prototype.similarity = function (otherVector) {\n return this.dot(otherVector) / this.magnitude() || 0\n}\n\n/**\n * Converts the vector to an array of the elements within the vector.\n *\n * @returns {Number[]}\n */\nlunr.Vector.prototype.toArray = function () {\n var output = new Array (this.elements.length / 2)\n\n for (var i = 1, j = 0; i < this.elements.length; i += 2, j++) {\n output[j] = this.elements[i]\n }\n\n return output\n}\n\n/**\n * A JSON serializable representation of the vector.\n *\n * @returns {Number[]}\n */\nlunr.Vector.prototype.toJSON = function () {\n return this.elements\n}\n/* eslint-disable */\n/*!\n * lunr.stemmer\n * Copyright (C) 2020 Oliver Nightingale\n * Includes code from - http://tartarus.org/~martin/PorterStemmer/js.txt\n */\n\n/**\n * lunr.stemmer is an english language stemmer, this is a JavaScript\n * implementation of the PorterStemmer taken from http://tartarus.org/~martin\n *\n * @static\n * @implements {lunr.PipelineFunction}\n * @param {lunr.Token} token - The string to stem\n * @returns {lunr.Token}\n * @see {@link lunr.Pipeline}\n * @function\n */\nlunr.stemmer = (function(){\n var step2list = {\n \"ational\" : \"ate\",\n \"tional\" : \"tion\",\n \"enci\" : \"ence\",\n \"anci\" : \"ance\",\n \"izer\" : \"ize\",\n \"bli\" : \"ble\",\n \"alli\" : \"al\",\n \"entli\" : \"ent\",\n \"eli\" : \"e\",\n \"ousli\" : \"ous\",\n \"ization\" : \"ize\",\n \"ation\" : \"ate\",\n \"ator\" : \"ate\",\n \"alism\" : \"al\",\n \"iveness\" : \"ive\",\n \"fulness\" : \"ful\",\n \"ousness\" : \"ous\",\n \"aliti\" : \"al\",\n \"iviti\" : \"ive\",\n \"biliti\" : \"ble\",\n \"logi\" : \"log\"\n },\n\n step3list = {\n \"icate\" : \"ic\",\n \"ative\" : \"\",\n \"alize\" : \"al\",\n \"iciti\" : \"ic\",\n \"ical\" : \"ic\",\n \"ful\" : \"\",\n \"ness\" : \"\"\n },\n\n c = \"[^aeiou]\", // consonant\n v = \"[aeiouy]\", // vowel\n C = c + \"[^aeiouy]*\", // consonant sequence\n V = v + \"[aeiou]*\", // vowel sequence\n\n mgr0 = \"^(\" + C + \")?\" + V + C, // [C]VC... is m>0\n meq1 = \"^(\" + C + \")?\" + V + C + \"(\" + V + \")?$\", // [C]VC[V] is m=1\n mgr1 = \"^(\" + C + \")?\" + V + C + V + C, // [C]VCVC... is m>1\n s_v = \"^(\" + C + \")?\" + v; // vowel in stem\n\n var re_mgr0 = new RegExp(mgr0);\n var re_mgr1 = new RegExp(mgr1);\n var re_meq1 = new RegExp(meq1);\n var re_s_v = new RegExp(s_v);\n\n var re_1a = /^(.+?)(ss|i)es$/;\n var re2_1a = /^(.+?)([^s])s$/;\n var re_1b = /^(.+?)eed$/;\n var re2_1b = /^(.+?)(ed|ing)$/;\n var re_1b_2 = /.$/;\n var re2_1b_2 = /(at|bl|iz)$/;\n var re3_1b_2 = new RegExp(\"([^aeiouylsz])\\\\1$\");\n var re4_1b_2 = new RegExp(\"^\" + C + v + \"[^aeiouwxy]$\");\n\n var re_1c = /^(.+?[^aeiou])y$/;\n var re_2 = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/;\n\n var re_3 = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/;\n\n var re_4 = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/;\n var re2_4 = /^(.+?)(s|t)(ion)$/;\n\n var re_5 = /^(.+?)e$/;\n var re_5_1 = /ll$/;\n var re3_5 = new RegExp(\"^\" + C + v + \"[^aeiouwxy]$\");\n\n var porterStemmer = function porterStemmer(w) {\n var stem,\n suffix,\n firstch,\n re,\n re2,\n re3,\n re4;\n\n if (w.length < 3) { return w; }\n\n firstch = w.substr(0,1);\n if (firstch == \"y\") {\n w = firstch.toUpperCase() + w.substr(1);\n }\n\n // Step 1a\n re = re_1a\n re2 = re2_1a;\n\n if (re.test(w)) { w = w.replace(re,\"$1$2\"); }\n else if (re2.test(w)) { w = w.replace(re2,\"$1$2\"); }\n\n // Step 1b\n re = re_1b;\n re2 = re2_1b;\n if (re.test(w)) {\n var fp = re.exec(w);\n re = re_mgr0;\n if (re.test(fp[1])) {\n re = re_1b_2;\n w = w.replace(re,\"\");\n }\n } else if (re2.test(w)) {\n var fp = re2.exec(w);\n stem = fp[1];\n re2 = re_s_v;\n if (re2.test(stem)) {\n w = stem;\n re2 = re2_1b_2;\n re3 = re3_1b_2;\n re4 = re4_1b_2;\n if (re2.test(w)) { w = w + \"e\"; }\n else if (re3.test(w)) { re = re_1b_2; w = w.replace(re,\"\"); }\n else if (re4.test(w)) { w = w + \"e\"; }\n }\n }\n\n // Step 1c - replace suffix y or Y by i if preceded by a non-vowel which is not the first letter of the word (so cry -> cri, by -> by, say -> say)\n re = re_1c;\n if (re.test(w)) {\n var fp = re.exec(w);\n stem = fp[1];\n w = stem + \"i\";\n }\n\n // Step 2\n re = re_2;\n if (re.test(w)) {\n var fp = re.exec(w);\n stem = fp[1];\n suffix = fp[2];\n re = re_mgr0;\n if (re.test(stem)) {\n w = stem + step2list[suffix];\n }\n }\n\n // Step 3\n re = re_3;\n if (re.test(w)) {\n var fp = re.exec(w);\n stem = fp[1];\n suffix = fp[2];\n re = re_mgr0;\n if (re.test(stem)) {\n w = stem + step3list[suffix];\n }\n }\n\n // Step 4\n re = re_4;\n re2 = re2_4;\n if (re.test(w)) {\n var fp = re.exec(w);\n stem = fp[1];\n re = re_mgr1;\n if (re.test(stem)) {\n w = stem;\n }\n } else if (re2.test(w)) {\n var fp = re2.exec(w);\n stem = fp[1] + fp[2];\n re2 = re_mgr1;\n if (re2.test(stem)) {\n w = stem;\n }\n }\n\n // Step 5\n re = re_5;\n if (re.test(w)) {\n var fp = re.exec(w);\n stem = fp[1];\n re = re_mgr1;\n re2 = re_meq1;\n re3 = re3_5;\n if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) {\n w = stem;\n }\n }\n\n re = re_5_1;\n re2 = re_mgr1;\n if (re.test(w) && re2.test(w)) {\n re = re_1b_2;\n w = w.replace(re,\"\");\n }\n\n // and turn initial Y back to y\n\n if (firstch == \"y\") {\n w = firstch.toLowerCase() + w.substr(1);\n }\n\n return w;\n };\n\n return function (token) {\n return token.update(porterStemmer);\n }\n})();\n\nlunr.Pipeline.registerFunction(lunr.stemmer, 'stemmer')\n/*!\n * lunr.stopWordFilter\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * lunr.generateStopWordFilter builds a stopWordFilter function from the provided\n * list of stop words.\n *\n * The built in lunr.stopWordFilter is built using this generator and can be used\n * to generate custom stopWordFilters for applications or non English languages.\n *\n * @function\n * @param {Array} token The token to pass through the filter\n * @returns {lunr.PipelineFunction}\n * @see lunr.Pipeline\n * @see lunr.stopWordFilter\n */\nlunr.generateStopWordFilter = function (stopWords) {\n var words = stopWords.reduce(function (memo, stopWord) {\n memo[stopWord] = stopWord\n return memo\n }, {})\n\n return function (token) {\n if (token && words[token.toString()] !== token.toString()) return token\n }\n}\n\n/**\n * lunr.stopWordFilter is an English language stop word list filter, any words\n * contained in the list will not be passed through the filter.\n *\n * This is intended to be used in the Pipeline. If the token does not pass the\n * filter then undefined will be returned.\n *\n * @function\n * @implements {lunr.PipelineFunction}\n * @params {lunr.Token} token - A token to check for being a stop word.\n * @returns {lunr.Token}\n * @see {@link lunr.Pipeline}\n */\nlunr.stopWordFilter = lunr.generateStopWordFilter([\n 'a',\n 'able',\n 'about',\n 'across',\n 'after',\n 'all',\n 'almost',\n 'also',\n 'am',\n 'among',\n 'an',\n 'and',\n 'any',\n 'are',\n 'as',\n 'at',\n 'be',\n 'because',\n 'been',\n 'but',\n 'by',\n 'can',\n 'cannot',\n 'could',\n 'dear',\n 'did',\n 'do',\n 'does',\n 'either',\n 'else',\n 'ever',\n 'every',\n 'for',\n 'from',\n 'get',\n 'got',\n 'had',\n 'has',\n 'have',\n 'he',\n 'her',\n 'hers',\n 'him',\n 'his',\n 'how',\n 'however',\n 'i',\n 'if',\n 'in',\n 'into',\n 'is',\n 'it',\n 'its',\n 'just',\n 'least',\n 'let',\n 'like',\n 'likely',\n 'may',\n 'me',\n 'might',\n 'most',\n 'must',\n 'my',\n 'neither',\n 'no',\n 'nor',\n 'not',\n 'of',\n 'off',\n 'often',\n 'on',\n 'only',\n 'or',\n 'other',\n 'our',\n 'own',\n 'rather',\n 'said',\n 'say',\n 'says',\n 'she',\n 'should',\n 'since',\n 'so',\n 'some',\n 'than',\n 'that',\n 'the',\n 'their',\n 'them',\n 'then',\n 'there',\n 'these',\n 'they',\n 'this',\n 'tis',\n 'to',\n 'too',\n 'twas',\n 'us',\n 'wants',\n 'was',\n 'we',\n 'were',\n 'what',\n 'when',\n 'where',\n 'which',\n 'while',\n 'who',\n 'whom',\n 'why',\n 'will',\n 'with',\n 'would',\n 'yet',\n 'you',\n 'your'\n])\n\nlunr.Pipeline.registerFunction(lunr.stopWordFilter, 'stopWordFilter')\n/*!\n * lunr.trimmer\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * lunr.trimmer is a pipeline function for trimming non word\n * characters from the beginning and end of tokens before they\n * enter the index.\n *\n * This implementation may not work correctly for non latin\n * characters and should either be removed or adapted for use\n * with languages with non-latin characters.\n *\n * @static\n * @implements {lunr.PipelineFunction}\n * @param {lunr.Token} token The token to pass through the filter\n * @returns {lunr.Token}\n * @see lunr.Pipeline\n */\nlunr.trimmer = function (token) {\n return token.update(function (s) {\n return s.replace(/^\\W+/, '').replace(/\\W+$/, '')\n })\n}\n\nlunr.Pipeline.registerFunction(lunr.trimmer, 'trimmer')\n/*!\n * lunr.TokenSet\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * A token set is used to store the unique list of all tokens\n * within an index. Token sets are also used to represent an\n * incoming query to the index, this query token set and index\n * token set are then intersected to find which tokens to look\n * up in the inverted index.\n *\n * A token set can hold multiple tokens, as in the case of the\n * index token set, or it can hold a single token as in the\n * case of a simple query token set.\n *\n * Additionally token sets are used to perform wildcard matching.\n * Leading, contained and trailing wildcards are supported, and\n * from this edit distance matching can also be provided.\n *\n * Token sets are implemented as a minimal finite state automata,\n * where both common prefixes and suffixes are shared between tokens.\n * This helps to reduce the space used for storing the token set.\n *\n * @constructor\n */\nlunr.TokenSet = function () {\n this.final = false\n this.edges = {}\n this.id = lunr.TokenSet._nextId\n lunr.TokenSet._nextId += 1\n}\n\n/**\n * Keeps track of the next, auto increment, identifier to assign\n * to a new tokenSet.\n *\n * TokenSets require a unique identifier to be correctly minimised.\n *\n * @private\n */\nlunr.TokenSet._nextId = 1\n\n/**\n * Creates a TokenSet instance from the given sorted array of words.\n *\n * @param {String[]} arr - A sorted array of strings to create the set from.\n * @returns {lunr.TokenSet}\n * @throws Will throw an error if the input array is not sorted.\n */\nlunr.TokenSet.fromArray = function (arr) {\n var builder = new lunr.TokenSet.Builder\n\n for (var i = 0, len = arr.length; i < len; i++) {\n builder.insert(arr[i])\n }\n\n builder.finish()\n return builder.root\n}\n\n/**\n * Creates a token set from a query clause.\n *\n * @private\n * @param {Object} clause - A single clause from lunr.Query.\n * @param {string} clause.term - The query clause term.\n * @param {number} [clause.editDistance] - The optional edit distance for the term.\n * @returns {lunr.TokenSet}\n */\nlunr.TokenSet.fromClause = function (clause) {\n if ('editDistance' in clause) {\n return lunr.TokenSet.fromFuzzyString(clause.term, clause.editDistance)\n } else {\n return lunr.TokenSet.fromString(clause.term)\n }\n}\n\n/**\n * Creates a token set representing a single string with a specified\n * edit distance.\n *\n * Insertions, deletions, substitutions and transpositions are each\n * treated as an edit distance of 1.\n *\n * Increasing the allowed edit distance will have a dramatic impact\n * on the performance of both creating and intersecting these TokenSets.\n * It is advised to keep the edit distance less than 3.\n *\n * @param {string} str - The string to create the token set from.\n * @param {number} editDistance - The allowed edit distance to match.\n * @returns {lunr.Vector}\n */\nlunr.TokenSet.fromFuzzyString = function (str, editDistance) {\n var root = new lunr.TokenSet\n\n var stack = [{\n node: root,\n editsRemaining: editDistance,\n str: str\n }]\n\n while (stack.length) {\n var frame = stack.pop()\n\n // no edit\n if (frame.str.length > 0) {\n var char = frame.str.charAt(0),\n noEditNode\n\n if (char in frame.node.edges) {\n noEditNode = frame.node.edges[char]\n } else {\n noEditNode = new lunr.TokenSet\n frame.node.edges[char] = noEditNode\n }\n\n if (frame.str.length == 1) {\n noEditNode.final = true\n }\n\n stack.push({\n node: noEditNode,\n editsRemaining: frame.editsRemaining,\n str: frame.str.slice(1)\n })\n }\n\n if (frame.editsRemaining == 0) {\n continue\n }\n\n // insertion\n if (\"*\" in frame.node.edges) {\n var insertionNode = frame.node.edges[\"*\"]\n } else {\n var insertionNode = new lunr.TokenSet\n frame.node.edges[\"*\"] = insertionNode\n }\n\n if (frame.str.length == 0) {\n insertionNode.final = true\n }\n\n stack.push({\n node: insertionNode,\n editsRemaining: frame.editsRemaining - 1,\n str: frame.str\n })\n\n // deletion\n // can only do a deletion if we have enough edits remaining\n // and if there are characters left to delete in the string\n if (frame.str.length > 1) {\n stack.push({\n node: frame.node,\n editsRemaining: frame.editsRemaining - 1,\n str: frame.str.slice(1)\n })\n }\n\n // deletion\n // just removing the last character from the str\n if (frame.str.length == 1) {\n frame.node.final = true\n }\n\n // substitution\n // can only do a substitution if we have enough edits remaining\n // and if there are characters left to substitute\n if (frame.str.length >= 1) {\n if (\"*\" in frame.node.edges) {\n var substitutionNode = frame.node.edges[\"*\"]\n } else {\n var substitutionNode = new lunr.TokenSet\n frame.node.edges[\"*\"] = substitutionNode\n }\n\n if (frame.str.length == 1) {\n substitutionNode.final = true\n }\n\n stack.push({\n node: substitutionNode,\n editsRemaining: frame.editsRemaining - 1,\n str: frame.str.slice(1)\n })\n }\n\n // transposition\n // can only do a transposition if there are edits remaining\n // and there are enough characters to transpose\n if (frame.str.length > 1) {\n var charA = frame.str.charAt(0),\n charB = frame.str.charAt(1),\n transposeNode\n\n if (charB in frame.node.edges) {\n transposeNode = frame.node.edges[charB]\n } else {\n transposeNode = new lunr.TokenSet\n frame.node.edges[charB] = transposeNode\n }\n\n if (frame.str.length == 1) {\n transposeNode.final = true\n }\n\n stack.push({\n node: transposeNode,\n editsRemaining: frame.editsRemaining - 1,\n str: charA + frame.str.slice(2)\n })\n }\n }\n\n return root\n}\n\n/**\n * Creates a TokenSet from a string.\n *\n * The string may contain one or more wildcard characters (*)\n * that will allow wildcard matching when intersecting with\n * another TokenSet.\n *\n * @param {string} str - The string to create a TokenSet from.\n * @returns {lunr.TokenSet}\n */\nlunr.TokenSet.fromString = function (str) {\n var node = new lunr.TokenSet,\n root = node\n\n /*\n * Iterates through all characters within the passed string\n * appending a node for each character.\n *\n * When a wildcard character is found then a self\n * referencing edge is introduced to continually match\n * any number of any characters.\n */\n for (var i = 0, len = str.length; i < len; i++) {\n var char = str[i],\n final = (i == len - 1)\n\n if (char == \"*\") {\n node.edges[char] = node\n node.final = final\n\n } else {\n var next = new lunr.TokenSet\n next.final = final\n\n node.edges[char] = next\n node = next\n }\n }\n\n return root\n}\n\n/**\n * Converts this TokenSet into an array of strings\n * contained within the TokenSet.\n *\n * This is not intended to be used on a TokenSet that\n * contains wildcards, in these cases the results are\n * undefined and are likely to cause an infinite loop.\n *\n * @returns {string[]}\n */\nlunr.TokenSet.prototype.toArray = function () {\n var words = []\n\n var stack = [{\n prefix: \"\",\n node: this\n }]\n\n while (stack.length) {\n var frame = stack.pop(),\n edges = Object.keys(frame.node.edges),\n len = edges.length\n\n if (frame.node.final) {\n /* In Safari, at this point the prefix is sometimes corrupted, see:\n * https://github.com/olivernn/lunr.js/issues/279 Calling any\n * String.prototype method forces Safari to \"cast\" this string to what\n * it's supposed to be, fixing the bug. */\n frame.prefix.charAt(0)\n words.push(frame.prefix)\n }\n\n for (var i = 0; i < len; i++) {\n var edge = edges[i]\n\n stack.push({\n prefix: frame.prefix.concat(edge),\n node: frame.node.edges[edge]\n })\n }\n }\n\n return words\n}\n\n/**\n * Generates a string representation of a TokenSet.\n *\n * This is intended to allow TokenSets to be used as keys\n * in objects, largely to aid the construction and minimisation\n * of a TokenSet. As such it is not designed to be a human\n * friendly representation of the TokenSet.\n *\n * @returns {string}\n */\nlunr.TokenSet.prototype.toString = function () {\n // NOTE: Using Object.keys here as this.edges is very likely\n // to enter 'hash-mode' with many keys being added\n //\n // avoiding a for-in loop here as it leads to the function\n // being de-optimised (at least in V8). From some simple\n // benchmarks the performance is comparable, but allowing\n // V8 to optimize may mean easy performance wins in the future.\n\n if (this._str) {\n return this._str\n }\n\n var str = this.final ? '1' : '0',\n labels = Object.keys(this.edges).sort(),\n len = labels.length\n\n for (var i = 0; i < len; i++) {\n var label = labels[i],\n node = this.edges[label]\n\n str = str + label + node.id\n }\n\n return str\n}\n\n/**\n * Returns a new TokenSet that is the intersection of\n * this TokenSet and the passed TokenSet.\n *\n * This intersection will take into account any wildcards\n * contained within the TokenSet.\n *\n * @param {lunr.TokenSet} b - An other TokenSet to intersect with.\n * @returns {lunr.TokenSet}\n */\nlunr.TokenSet.prototype.intersect = function (b) {\n var output = new lunr.TokenSet,\n frame = undefined\n\n var stack = [{\n qNode: b,\n output: output,\n node: this\n }]\n\n while (stack.length) {\n frame = stack.pop()\n\n // NOTE: As with the #toString method, we are using\n // Object.keys and a for loop instead of a for-in loop\n // as both of these objects enter 'hash' mode, causing\n // the function to be de-optimised in V8\n var qEdges = Object.keys(frame.qNode.edges),\n qLen = qEdges.length,\n nEdges = Object.keys(frame.node.edges),\n nLen = nEdges.length\n\n for (var q = 0; q < qLen; q++) {\n var qEdge = qEdges[q]\n\n for (var n = 0; n < nLen; n++) {\n var nEdge = nEdges[n]\n\n if (nEdge == qEdge || qEdge == '*') {\n var node = frame.node.edges[nEdge],\n qNode = frame.qNode.edges[qEdge],\n final = node.final && qNode.final,\n next = undefined\n\n if (nEdge in frame.output.edges) {\n // an edge already exists for this character\n // no need to create a new node, just set the finality\n // bit unless this node is already final\n next = frame.output.edges[nEdge]\n next.final = next.final || final\n\n } else {\n // no edge exists yet, must create one\n // set the finality bit and insert it\n // into the output\n next = new lunr.TokenSet\n next.final = final\n frame.output.edges[nEdge] = next\n }\n\n stack.push({\n qNode: qNode,\n output: next,\n node: node\n })\n }\n }\n }\n }\n\n return output\n}\nlunr.TokenSet.Builder = function () {\n this.previousWord = \"\"\n this.root = new lunr.TokenSet\n this.uncheckedNodes = []\n this.minimizedNodes = {}\n}\n\nlunr.TokenSet.Builder.prototype.insert = function (word) {\n var node,\n commonPrefix = 0\n\n if (word < this.previousWord) {\n throw new Error (\"Out of order word insertion\")\n }\n\n for (var i = 0; i < word.length && i < this.previousWord.length; i++) {\n if (word[i] != this.previousWord[i]) break\n commonPrefix++\n }\n\n this.minimize(commonPrefix)\n\n if (this.uncheckedNodes.length == 0) {\n node = this.root\n } else {\n node = this.uncheckedNodes[this.uncheckedNodes.length - 1].child\n }\n\n for (var i = commonPrefix; i < word.length; i++) {\n var nextNode = new lunr.TokenSet,\n char = word[i]\n\n node.edges[char] = nextNode\n\n this.uncheckedNodes.push({\n parent: node,\n char: char,\n child: nextNode\n })\n\n node = nextNode\n }\n\n node.final = true\n this.previousWord = word\n}\n\nlunr.TokenSet.Builder.prototype.finish = function () {\n this.minimize(0)\n}\n\nlunr.TokenSet.Builder.prototype.minimize = function (downTo) {\n for (var i = this.uncheckedNodes.length - 1; i >= downTo; i--) {\n var node = this.uncheckedNodes[i],\n childKey = node.child.toString()\n\n if (childKey in this.minimizedNodes) {\n node.parent.edges[node.char] = this.minimizedNodes[childKey]\n } else {\n // Cache the key for this node since\n // we know it can't change anymore\n node.child._str = childKey\n\n this.minimizedNodes[childKey] = node.child\n }\n\n this.uncheckedNodes.pop()\n }\n}\n/*!\n * lunr.Index\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * An index contains the built index of all documents and provides a query interface\n * to the index.\n *\n * Usually instances of lunr.Index will not be created using this constructor, instead\n * lunr.Builder should be used to construct new indexes, or lunr.Index.load should be\n * used to load previously built and serialized indexes.\n *\n * @constructor\n * @param {Object} attrs - The attributes of the built search index.\n * @param {Object} attrs.invertedIndex - An index of term/field to document reference.\n * @param {Object} attrs.fieldVectors - Field vectors\n * @param {lunr.TokenSet} attrs.tokenSet - An set of all corpus tokens.\n * @param {string[]} attrs.fields - The names of indexed document fields.\n * @param {lunr.Pipeline} attrs.pipeline - The pipeline to use for search terms.\n */\nlunr.Index = function (attrs) {\n this.invertedIndex = attrs.invertedIndex\n this.fieldVectors = attrs.fieldVectors\n this.tokenSet = attrs.tokenSet\n this.fields = attrs.fields\n this.pipeline = attrs.pipeline\n}\n\n/**\n * A result contains details of a document matching a search query.\n * @typedef {Object} lunr.Index~Result\n * @property {string} ref - The reference of the document this result represents.\n * @property {number} score - A number between 0 and 1 representing how similar this document is to the query.\n * @property {lunr.MatchData} matchData - Contains metadata about this match including which term(s) caused the match.\n */\n\n/**\n * Although lunr provides the ability to create queries using lunr.Query, it also provides a simple\n * query language which itself is parsed into an instance of lunr.Query.\n *\n * For programmatically building queries it is advised to directly use lunr.Query, the query language\n * is best used for human entered text rather than program generated text.\n *\n * At its simplest queries can just be a single term, e.g. `hello`, multiple terms are also supported\n * and will be combined with OR, e.g `hello world` will match documents that contain either 'hello'\n * or 'world', though those that contain both will rank higher in the results.\n *\n * Wildcards can be included in terms to match one or more unspecified characters, these wildcards can\n * be inserted anywhere within the term, and more than one wildcard can exist in a single term. Adding\n * wildcards will increase the number of documents that will be found but can also have a negative\n * impact on query performance, especially with wildcards at the beginning of a term.\n *\n * Terms can be restricted to specific fields, e.g. `title:hello`, only documents with the term\n * hello in the title field will match this query. Using a field not present in the index will lead\n * to an error being thrown.\n *\n * Modifiers can also be added to terms, lunr supports edit distance and boost modifiers on terms. A term\n * boost will make documents matching that term score higher, e.g. `foo^5`. Edit distance is also supported\n * to provide fuzzy matching, e.g. 'hello~2' will match documents with hello with an edit distance of 2.\n * Avoid large values for edit distance to improve query performance.\n *\n * Each term also supports a presence modifier. By default a term's presence in document is optional, however\n * this can be changed to either required or prohibited. For a term's presence to be required in a document the\n * term should be prefixed with a '+', e.g. `+foo bar` is a search for documents that must contain 'foo' and\n * optionally contain 'bar'. Conversely a leading '-' sets the terms presence to prohibited, i.e. it must not\n * appear in a document, e.g. `-foo bar` is a search for documents that do not contain 'foo' but may contain 'bar'.\n *\n * To escape special characters the backslash character '\\' can be used, this allows searches to include\n * characters that would normally be considered modifiers, e.g. `foo\\~2` will search for a term \"foo~2\" instead\n * of attempting to apply a boost of 2 to the search term \"foo\".\n *\n * @typedef {string} lunr.Index~QueryString\n * @example Simple single term query\n * hello\n * @example Multiple term query\n * hello world\n * @example term scoped to a field\n * title:hello\n * @example term with a boost of 10\n * hello^10\n * @example term with an edit distance of 2\n * hello~2\n * @example terms with presence modifiers\n * -foo +bar baz\n */\n\n/**\n * Performs a search against the index using lunr query syntax.\n *\n * Results will be returned sorted by their score, the most relevant results\n * will be returned first. For details on how the score is calculated, please see\n * the {@link https://lunrjs.com/guides/searching.html#scoring|guide}.\n *\n * For more programmatic querying use lunr.Index#query.\n *\n * @param {lunr.Index~QueryString} queryString - A string containing a lunr query.\n * @throws {lunr.QueryParseError} If the passed query string cannot be parsed.\n * @returns {lunr.Index~Result[]}\n */\nlunr.Index.prototype.search = function (queryString) {\n return this.query(function (query) {\n var parser = new lunr.QueryParser(queryString, query)\n parser.parse()\n })\n}\n\n/**\n * A query builder callback provides a query object to be used to express\n * the query to perform on the index.\n *\n * @callback lunr.Index~queryBuilder\n * @param {lunr.Query} query - The query object to build up.\n * @this lunr.Query\n */\n\n/**\n * Performs a query against the index using the yielded lunr.Query object.\n *\n * If performing programmatic queries against the index, this method is preferred\n * over lunr.Index#search so as to avoid the additional query parsing overhead.\n *\n * A query object is yielded to the supplied function which should be used to\n * express the query to be run against the index.\n *\n * Note that although this function takes a callback parameter it is _not_ an\n * asynchronous operation, the callback is just yielded a query object to be\n * customized.\n *\n * @param {lunr.Index~queryBuilder} fn - A function that is used to build the query.\n * @returns {lunr.Index~Result[]}\n */\nlunr.Index.prototype.query = function (fn) {\n // for each query clause\n // * process terms\n // * expand terms from token set\n // * find matching documents and metadata\n // * get document vectors\n // * score documents\n\n var query = new lunr.Query(this.fields),\n matchingFields = Object.create(null),\n queryVectors = Object.create(null),\n termFieldCache = Object.create(null),\n requiredMatches = Object.create(null),\n prohibitedMatches = Object.create(null)\n\n /*\n * To support field level boosts a query vector is created per\n * field. An empty vector is eagerly created to support negated\n * queries.\n */\n for (var i = 0; i < this.fields.length; i++) {\n queryVectors[this.fields[i]] = new lunr.Vector\n }\n\n fn.call(query, query)\n\n for (var i = 0; i < query.clauses.length; i++) {\n /*\n * Unless the pipeline has been disabled for this term, which is\n * the case for terms with wildcards, we need to pass the clause\n * term through the search pipeline. A pipeline returns an array\n * of processed terms. Pipeline functions may expand the passed\n * term, which means we may end up performing multiple index lookups\n * for a single query term.\n */\n var clause = query.clauses[i],\n terms = null,\n clauseMatches = lunr.Set.empty\n\n if (clause.usePipeline) {\n terms = this.pipeline.runString(clause.term, {\n fields: clause.fields\n })\n } else {\n terms = [clause.term]\n }\n\n for (var m = 0; m < terms.length; m++) {\n var term = terms[m]\n\n /*\n * Each term returned from the pipeline needs to use the same query\n * clause object, e.g. the same boost and or edit distance. The\n * simplest way to do this is to re-use the clause object but mutate\n * its term property.\n */\n clause.term = term\n\n /*\n * From the term in the clause we create a token set which will then\n * be used to intersect the indexes token set to get a list of terms\n * to lookup in the inverted index\n */\n var termTokenSet = lunr.TokenSet.fromClause(clause),\n expandedTerms = this.tokenSet.intersect(termTokenSet).toArray()\n\n /*\n * If a term marked as required does not exist in the tokenSet it is\n * impossible for the search to return any matches. We set all the field\n * scoped required matches set to empty and stop examining any further\n * clauses.\n */\n if (expandedTerms.length === 0 && clause.presence === lunr.Query.presence.REQUIRED) {\n for (var k = 0; k < clause.fields.length; k++) {\n var field = clause.fields[k]\n requiredMatches[field] = lunr.Set.empty\n }\n\n break\n }\n\n for (var j = 0; j < expandedTerms.length; j++) {\n /*\n * For each term get the posting and termIndex, this is required for\n * building the query vector.\n */\n var expandedTerm = expandedTerms[j],\n posting = this.invertedIndex[expandedTerm],\n termIndex = posting._index\n\n for (var k = 0; k < clause.fields.length; k++) {\n /*\n * For each field that this query term is scoped by (by default\n * all fields are in scope) we need to get all the document refs\n * that have this term in that field.\n *\n * The posting is the entry in the invertedIndex for the matching\n * term from above.\n */\n var field = clause.fields[k],\n fieldPosting = posting[field],\n matchingDocumentRefs = Object.keys(fieldPosting),\n termField = expandedTerm + \"/\" + field,\n matchingDocumentsSet = new lunr.Set(matchingDocumentRefs)\n\n /*\n * if the presence of this term is required ensure that the matching\n * documents are added to the set of required matches for this clause.\n *\n */\n if (clause.presence == lunr.Query.presence.REQUIRED) {\n clauseMatches = clauseMatches.union(matchingDocumentsSet)\n\n if (requiredMatches[field] === undefined) {\n requiredMatches[field] = lunr.Set.complete\n }\n }\n\n /*\n * if the presence of this term is prohibited ensure that the matching\n * documents are added to the set of prohibited matches for this field,\n * creating that set if it does not yet exist.\n */\n if (clause.presence == lunr.Query.presence.PROHIBITED) {\n if (prohibitedMatches[field] === undefined) {\n prohibitedMatches[field] = lunr.Set.empty\n }\n\n prohibitedMatches[field] = prohibitedMatches[field].union(matchingDocumentsSet)\n\n /*\n * Prohibited matches should not be part of the query vector used for\n * similarity scoring and no metadata should be extracted so we continue\n * to the next field\n */\n continue\n }\n\n /*\n * The query field vector is populated using the termIndex found for\n * the term and a unit value with the appropriate boost applied.\n * Using upsert because there could already be an entry in the vector\n * for the term we are working with. In that case we just add the scores\n * together.\n */\n queryVectors[field].upsert(termIndex, clause.boost, function (a, b) { return a + b })\n\n /**\n * If we've already seen this term, field combo then we've already collected\n * the matching documents and metadata, no need to go through all that again\n */\n if (termFieldCache[termField]) {\n continue\n }\n\n for (var l = 0; l < matchingDocumentRefs.length; l++) {\n /*\n * All metadata for this term/field/document triple\n * are then extracted and collected into an instance\n * of lunr.MatchData ready to be returned in the query\n * results\n */\n var matchingDocumentRef = matchingDocumentRefs[l],\n matchingFieldRef = new lunr.FieldRef (matchingDocumentRef, field),\n metadata = fieldPosting[matchingDocumentRef],\n fieldMatch\n\n if ((fieldMatch = matchingFields[matchingFieldRef]) === undefined) {\n matchingFields[matchingFieldRef] = new lunr.MatchData (expandedTerm, field, metadata)\n } else {\n fieldMatch.add(expandedTerm, field, metadata)\n }\n\n }\n\n termFieldCache[termField] = true\n }\n }\n }\n\n /**\n * If the presence was required we need to update the requiredMatches field sets.\n * We do this after all fields for the term have collected their matches because\n * the clause terms presence is required in _any_ of the fields not _all_ of the\n * fields.\n */\n if (clause.presence === lunr.Query.presence.REQUIRED) {\n for (var k = 0; k < clause.fields.length; k++) {\n var field = clause.fields[k]\n requiredMatches[field] = requiredMatches[field].intersect(clauseMatches)\n }\n }\n }\n\n /**\n * Need to combine the field scoped required and prohibited\n * matching documents into a global set of required and prohibited\n * matches\n */\n var allRequiredMatches = lunr.Set.complete,\n allProhibitedMatches = lunr.Set.empty\n\n for (var i = 0; i < this.fields.length; i++) {\n var field = this.fields[i]\n\n if (requiredMatches[field]) {\n allRequiredMatches = allRequiredMatches.intersect(requiredMatches[field])\n }\n\n if (prohibitedMatches[field]) {\n allProhibitedMatches = allProhibitedMatches.union(prohibitedMatches[field])\n }\n }\n\n var matchingFieldRefs = Object.keys(matchingFields),\n results = [],\n matches = Object.create(null)\n\n /*\n * If the query is negated (contains only prohibited terms)\n * we need to get _all_ fieldRefs currently existing in the\n * index. This is only done when we know that the query is\n * entirely prohibited terms to avoid any cost of getting all\n * fieldRefs unnecessarily.\n *\n * Additionally, blank MatchData must be created to correctly\n * populate the results.\n */\n if (query.isNegated()) {\n matchingFieldRefs = Object.keys(this.fieldVectors)\n\n for (var i = 0; i < matchingFieldRefs.length; i++) {\n var matchingFieldRef = matchingFieldRefs[i]\n var fieldRef = lunr.FieldRef.fromString(matchingFieldRef)\n matchingFields[matchingFieldRef] = new lunr.MatchData\n }\n }\n\n for (var i = 0; i < matchingFieldRefs.length; i++) {\n /*\n * Currently we have document fields that match the query, but we\n * need to return documents. The matchData and scores are combined\n * from multiple fields belonging to the same document.\n *\n * Scores are calculated by field, using the query vectors created\n * above, and combined into a final document score using addition.\n */\n var fieldRef = lunr.FieldRef.fromString(matchingFieldRefs[i]),\n docRef = fieldRef.docRef\n\n if (!allRequiredMatches.contains(docRef)) {\n continue\n }\n\n if (allProhibitedMatches.contains(docRef)) {\n continue\n }\n\n var fieldVector = this.fieldVectors[fieldRef],\n score = queryVectors[fieldRef.fieldName].similarity(fieldVector),\n docMatch\n\n if ((docMatch = matches[docRef]) !== undefined) {\n docMatch.score += score\n docMatch.matchData.combine(matchingFields[fieldRef])\n } else {\n var match = {\n ref: docRef,\n score: score,\n matchData: matchingFields[fieldRef]\n }\n matches[docRef] = match\n results.push(match)\n }\n }\n\n /*\n * Sort the results objects by score, highest first.\n */\n return results.sort(function (a, b) {\n return b.score - a.score\n })\n}\n\n/**\n * Prepares the index for JSON serialization.\n *\n * The schema for this JSON blob will be described in a\n * separate JSON schema file.\n *\n * @returns {Object}\n */\nlunr.Index.prototype.toJSON = function () {\n var invertedIndex = Object.keys(this.invertedIndex)\n .sort()\n .map(function (term) {\n return [term, this.invertedIndex[term]]\n }, this)\n\n var fieldVectors = Object.keys(this.fieldVectors)\n .map(function (ref) {\n return [ref, this.fieldVectors[ref].toJSON()]\n }, this)\n\n return {\n version: lunr.version,\n fields: this.fields,\n fieldVectors: fieldVectors,\n invertedIndex: invertedIndex,\n pipeline: this.pipeline.toJSON()\n }\n}\n\n/**\n * Loads a previously serialized lunr.Index\n *\n * @param {Object} serializedIndex - A previously serialized lunr.Index\n * @returns {lunr.Index}\n */\nlunr.Index.load = function (serializedIndex) {\n var attrs = {},\n fieldVectors = {},\n serializedVectors = serializedIndex.fieldVectors,\n invertedIndex = Object.create(null),\n serializedInvertedIndex = serializedIndex.invertedIndex,\n tokenSetBuilder = new lunr.TokenSet.Builder,\n pipeline = lunr.Pipeline.load(serializedIndex.pipeline)\n\n if (serializedIndex.version != lunr.version) {\n lunr.utils.warn(\"Version mismatch when loading serialised index. Current version of lunr '\" + lunr.version + \"' does not match serialized index '\" + serializedIndex.version + \"'\")\n }\n\n for (var i = 0; i < serializedVectors.length; i++) {\n var tuple = serializedVectors[i],\n ref = tuple[0],\n elements = tuple[1]\n\n fieldVectors[ref] = new lunr.Vector(elements)\n }\n\n for (var i = 0; i < serializedInvertedIndex.length; i++) {\n var tuple = serializedInvertedIndex[i],\n term = tuple[0],\n posting = tuple[1]\n\n tokenSetBuilder.insert(term)\n invertedIndex[term] = posting\n }\n\n tokenSetBuilder.finish()\n\n attrs.fields = serializedIndex.fields\n\n attrs.fieldVectors = fieldVectors\n attrs.invertedIndex = invertedIndex\n attrs.tokenSet = tokenSetBuilder.root\n attrs.pipeline = pipeline\n\n return new lunr.Index(attrs)\n}\n/*!\n * lunr.Builder\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * lunr.Builder performs indexing on a set of documents and\n * returns instances of lunr.Index ready for querying.\n *\n * All configuration of the index is done via the builder, the\n * fields to index, the document reference, the text processing\n * pipeline and document scoring parameters are all set on the\n * builder before indexing.\n *\n * @constructor\n * @property {string} _ref - Internal reference to the document reference field.\n * @property {string[]} _fields - Internal reference to the document fields to index.\n * @property {object} invertedIndex - The inverted index maps terms to document fields.\n * @property {object} documentTermFrequencies - Keeps track of document term frequencies.\n * @property {object} documentLengths - Keeps track of the length of documents added to the index.\n * @property {lunr.tokenizer} tokenizer - Function for splitting strings into tokens for indexing.\n * @property {lunr.Pipeline} pipeline - The pipeline performs text processing on tokens before indexing.\n * @property {lunr.Pipeline} searchPipeline - A pipeline for processing search terms before querying the index.\n * @property {number} documentCount - Keeps track of the total number of documents indexed.\n * @property {number} _b - A parameter to control field length normalization, setting this to 0 disabled normalization, 1 fully normalizes field lengths, the default value is 0.75.\n * @property {number} _k1 - A parameter to control how quickly an increase in term frequency results in term frequency saturation, the default value is 1.2.\n * @property {number} termIndex - A counter incremented for each unique term, used to identify a terms position in the vector space.\n * @property {array} metadataWhitelist - A list of metadata keys that have been whitelisted for entry in the index.\n */\nlunr.Builder = function () {\n this._ref = \"id\"\n this._fields = Object.create(null)\n this._documents = Object.create(null)\n this.invertedIndex = Object.create(null)\n this.fieldTermFrequencies = {}\n this.fieldLengths = {}\n this.tokenizer = lunr.tokenizer\n this.pipeline = new lunr.Pipeline\n this.searchPipeline = new lunr.Pipeline\n this.documentCount = 0\n this._b = 0.75\n this._k1 = 1.2\n this.termIndex = 0\n this.metadataWhitelist = []\n}\n\n/**\n * Sets the document field used as the document reference. Every document must have this field.\n * The type of this field in the document should be a string, if it is not a string it will be\n * coerced into a string by calling toString.\n *\n * The default ref is 'id'.\n *\n * The ref should _not_ be changed during indexing, it should be set before any documents are\n * added to the index. Changing it during indexing can lead to inconsistent results.\n *\n * @param {string} ref - The name of the reference field in the document.\n */\nlunr.Builder.prototype.ref = function (ref) {\n this._ref = ref\n}\n\n/**\n * A function that is used to extract a field from a document.\n *\n * Lunr expects a field to be at the top level of a document, if however the field\n * is deeply nested within a document an extractor function can be used to extract\n * the right field for indexing.\n *\n * @callback fieldExtractor\n * @param {object} doc - The document being added to the index.\n * @returns {?(string|object|object[])} obj - The object that will be indexed for this field.\n * @example Extracting a nested field\n * function (doc) { return doc.nested.field }\n */\n\n/**\n * Adds a field to the list of document fields that will be indexed. Every document being\n * indexed should have this field. Null values for this field in indexed documents will\n * not cause errors but will limit the chance of that document being retrieved by searches.\n *\n * All fields should be added before adding documents to the index. Adding fields after\n * a document has been indexed will have no effect on already indexed documents.\n *\n * Fields can be boosted at build time. This allows terms within that field to have more\n * importance when ranking search results. Use a field boost to specify that matches within\n * one field are more important than other fields.\n *\n * @param {string} fieldName - The name of a field to index in all documents.\n * @param {object} attributes - Optional attributes associated with this field.\n * @param {number} [attributes.boost=1] - Boost applied to all terms within this field.\n * @param {fieldExtractor} [attributes.extractor] - Function to extract a field from a document.\n * @throws {RangeError} fieldName cannot contain unsupported characters '/'\n */\nlunr.Builder.prototype.field = function (fieldName, attributes) {\n if (/\\//.test(fieldName)) {\n throw new RangeError (\"Field '\" + fieldName + \"' contains illegal character '/'\")\n }\n\n this._fields[fieldName] = attributes || {}\n}\n\n/**\n * A parameter to tune the amount of field length normalisation that is applied when\n * calculating relevance scores. A value of 0 will completely disable any normalisation\n * and a value of 1 will fully normalise field lengths. The default is 0.75. Values of b\n * will be clamped to the range 0 - 1.\n *\n * @param {number} number - The value to set for this tuning parameter.\n */\nlunr.Builder.prototype.b = function (number) {\n if (number < 0) {\n this._b = 0\n } else if (number > 1) {\n this._b = 1\n } else {\n this._b = number\n }\n}\n\n/**\n * A parameter that controls the speed at which a rise in term frequency results in term\n * frequency saturation. The default value is 1.2. Setting this to a higher value will give\n * slower saturation levels, a lower value will result in quicker saturation.\n *\n * @param {number} number - The value to set for this tuning parameter.\n */\nlunr.Builder.prototype.k1 = function (number) {\n this._k1 = number\n}\n\n/**\n * Adds a document to the index.\n *\n * Before adding fields to the index the index should have been fully setup, with the document\n * ref and all fields to index already having been specified.\n *\n * The document must have a field name as specified by the ref (by default this is 'id') and\n * it should have all fields defined for indexing, though null or undefined values will not\n * cause errors.\n *\n * Entire documents can be boosted at build time. Applying a boost to a document indicates that\n * this document should rank higher in search results than other documents.\n *\n * @param {object} doc - The document to add to the index.\n * @param {object} attributes - Optional attributes associated with this document.\n * @param {number} [attributes.boost=1] - Boost applied to all terms within this document.\n */\nlunr.Builder.prototype.add = function (doc, attributes) {\n var docRef = doc[this._ref],\n fields = Object.keys(this._fields)\n\n this._documents[docRef] = attributes || {}\n this.documentCount += 1\n\n for (var i = 0; i < fields.length; i++) {\n var fieldName = fields[i],\n extractor = this._fields[fieldName].extractor,\n field = extractor ? extractor(doc) : doc[fieldName],\n tokens = this.tokenizer(field, {\n fields: [fieldName]\n }),\n terms = this.pipeline.run(tokens),\n fieldRef = new lunr.FieldRef (docRef, fieldName),\n fieldTerms = Object.create(null)\n\n this.fieldTermFrequencies[fieldRef] = fieldTerms\n this.fieldLengths[fieldRef] = 0\n\n // store the length of this field for this document\n this.fieldLengths[fieldRef] += terms.length\n\n // calculate term frequencies for this field\n for (var j = 0; j < terms.length; j++) {\n var term = terms[j]\n\n if (fieldTerms[term] == undefined) {\n fieldTerms[term] = 0\n }\n\n fieldTerms[term] += 1\n\n // add to inverted index\n // create an initial posting if one doesn't exist\n if (this.invertedIndex[term] == undefined) {\n var posting = Object.create(null)\n posting[\"_index\"] = this.termIndex\n this.termIndex += 1\n\n for (var k = 0; k < fields.length; k++) {\n posting[fields[k]] = Object.create(null)\n }\n\n this.invertedIndex[term] = posting\n }\n\n // add an entry for this term/fieldName/docRef to the invertedIndex\n if (this.invertedIndex[term][fieldName][docRef] == undefined) {\n this.invertedIndex[term][fieldName][docRef] = Object.create(null)\n }\n\n // store all whitelisted metadata about this token in the\n // inverted index\n for (var l = 0; l < this.metadataWhitelist.length; l++) {\n var metadataKey = this.metadataWhitelist[l],\n metadata = term.metadata[metadataKey]\n\n if (this.invertedIndex[term][fieldName][docRef][metadataKey] == undefined) {\n this.invertedIndex[term][fieldName][docRef][metadataKey] = []\n }\n\n this.invertedIndex[term][fieldName][docRef][metadataKey].push(metadata)\n }\n }\n\n }\n}\n\n/**\n * Calculates the average document length for this index\n *\n * @private\n */\nlunr.Builder.prototype.calculateAverageFieldLengths = function () {\n\n var fieldRefs = Object.keys(this.fieldLengths),\n numberOfFields = fieldRefs.length,\n accumulator = {},\n documentsWithField = {}\n\n for (var i = 0; i < numberOfFields; i++) {\n var fieldRef = lunr.FieldRef.fromString(fieldRefs[i]),\n field = fieldRef.fieldName\n\n documentsWithField[field] || (documentsWithField[field] = 0)\n documentsWithField[field] += 1\n\n accumulator[field] || (accumulator[field] = 0)\n accumulator[field] += this.fieldLengths[fieldRef]\n }\n\n var fields = Object.keys(this._fields)\n\n for (var i = 0; i < fields.length; i++) {\n var fieldName = fields[i]\n accumulator[fieldName] = accumulator[fieldName] / documentsWithField[fieldName]\n }\n\n this.averageFieldLength = accumulator\n}\n\n/**\n * Builds a vector space model of every document using lunr.Vector\n *\n * @private\n */\nlunr.Builder.prototype.createFieldVectors = function () {\n var fieldVectors = {},\n fieldRefs = Object.keys(this.fieldTermFrequencies),\n fieldRefsLength = fieldRefs.length,\n termIdfCache = Object.create(null)\n\n for (var i = 0; i < fieldRefsLength; i++) {\n var fieldRef = lunr.FieldRef.fromString(fieldRefs[i]),\n fieldName = fieldRef.fieldName,\n fieldLength = this.fieldLengths[fieldRef],\n fieldVector = new lunr.Vector,\n termFrequencies = this.fieldTermFrequencies[fieldRef],\n terms = Object.keys(termFrequencies),\n termsLength = terms.length\n\n\n var fieldBoost = this._fields[fieldName].boost || 1,\n docBoost = this._documents[fieldRef.docRef].boost || 1\n\n for (var j = 0; j < termsLength; j++) {\n var term = terms[j],\n tf = termFrequencies[term],\n termIndex = this.invertedIndex[term]._index,\n idf, score, scoreWithPrecision\n\n if (termIdfCache[term] === undefined) {\n idf = lunr.idf(this.invertedIndex[term], this.documentCount)\n termIdfCache[term] = idf\n } else {\n idf = termIdfCache[term]\n }\n\n score = idf * ((this._k1 + 1) * tf) / (this._k1 * (1 - this._b + this._b * (fieldLength / this.averageFieldLength[fieldName])) + tf)\n score *= fieldBoost\n score *= docBoost\n scoreWithPrecision = Math.round(score * 1000) / 1000\n // Converts 1.23456789 to 1.234.\n // Reducing the precision so that the vectors take up less\n // space when serialised. Doing it now so that they behave\n // the same before and after serialisation. Also, this is\n // the fastest approach to reducing a number's precision in\n // JavaScript.\n\n fieldVector.insert(termIndex, scoreWithPrecision)\n }\n\n fieldVectors[fieldRef] = fieldVector\n }\n\n this.fieldVectors = fieldVectors\n}\n\n/**\n * Creates a token set of all tokens in the index using lunr.TokenSet\n *\n * @private\n */\nlunr.Builder.prototype.createTokenSet = function () {\n this.tokenSet = lunr.TokenSet.fromArray(\n Object.keys(this.invertedIndex).sort()\n )\n}\n\n/**\n * Builds the index, creating an instance of lunr.Index.\n *\n * This completes the indexing process and should only be called\n * once all documents have been added to the index.\n *\n * @returns {lunr.Index}\n */\nlunr.Builder.prototype.build = function () {\n this.calculateAverageFieldLengths()\n this.createFieldVectors()\n this.createTokenSet()\n\n return new lunr.Index({\n invertedIndex: this.invertedIndex,\n fieldVectors: this.fieldVectors,\n tokenSet: this.tokenSet,\n fields: Object.keys(this._fields),\n pipeline: this.searchPipeline\n })\n}\n\n/**\n * Applies a plugin to the index builder.\n *\n * A plugin is a function that is called with the index builder as its context.\n * Plugins can be used to customise or extend the behaviour of the index\n * in some way. A plugin is just a function, that encapsulated the custom\n * behaviour that should be applied when building the index.\n *\n * The plugin function will be called with the index builder as its argument, additional\n * arguments can also be passed when calling use. The function will be called\n * with the index builder as its context.\n *\n * @param {Function} plugin The plugin to apply.\n */\nlunr.Builder.prototype.use = function (fn) {\n var args = Array.prototype.slice.call(arguments, 1)\n args.unshift(this)\n fn.apply(this, args)\n}\n/**\n * Contains and collects metadata about a matching document.\n * A single instance of lunr.MatchData is returned as part of every\n * lunr.Index~Result.\n *\n * @constructor\n * @param {string} term - The term this match data is associated with\n * @param {string} field - The field in which the term was found\n * @param {object} metadata - The metadata recorded about this term in this field\n * @property {object} metadata - A cloned collection of metadata associated with this document.\n * @see {@link lunr.Index~Result}\n */\nlunr.MatchData = function (term, field, metadata) {\n var clonedMetadata = Object.create(null),\n metadataKeys = Object.keys(metadata || {})\n\n // Cloning the metadata to prevent the original\n // being mutated during match data combination.\n // Metadata is kept in an array within the inverted\n // index so cloning the data can be done with\n // Array#slice\n for (var i = 0; i < metadataKeys.length; i++) {\n var key = metadataKeys[i]\n clonedMetadata[key] = metadata[key].slice()\n }\n\n this.metadata = Object.create(null)\n\n if (term !== undefined) {\n this.metadata[term] = Object.create(null)\n this.metadata[term][field] = clonedMetadata\n }\n}\n\n/**\n * An instance of lunr.MatchData will be created for every term that matches a\n * document. However only one instance is required in a lunr.Index~Result. This\n * method combines metadata from another instance of lunr.MatchData with this\n * objects metadata.\n *\n * @param {lunr.MatchData} otherMatchData - Another instance of match data to merge with this one.\n * @see {@link lunr.Index~Result}\n */\nlunr.MatchData.prototype.combine = function (otherMatchData) {\n var terms = Object.keys(otherMatchData.metadata)\n\n for (var i = 0; i < terms.length; i++) {\n var term = terms[i],\n fields = Object.keys(otherMatchData.metadata[term])\n\n if (this.metadata[term] == undefined) {\n this.metadata[term] = Object.create(null)\n }\n\n for (var j = 0; j < fields.length; j++) {\n var field = fields[j],\n keys = Object.keys(otherMatchData.metadata[term][field])\n\n if (this.metadata[term][field] == undefined) {\n this.metadata[term][field] = Object.create(null)\n }\n\n for (var k = 0; k < keys.length; k++) {\n var key = keys[k]\n\n if (this.metadata[term][field][key] == undefined) {\n this.metadata[term][field][key] = otherMatchData.metadata[term][field][key]\n } else {\n this.metadata[term][field][key] = this.metadata[term][field][key].concat(otherMatchData.metadata[term][field][key])\n }\n\n }\n }\n }\n}\n\n/**\n * Add metadata for a term/field pair to this instance of match data.\n *\n * @param {string} term - The term this match data is associated with\n * @param {string} field - The field in which the term was found\n * @param {object} metadata - The metadata recorded about this term in this field\n */\nlunr.MatchData.prototype.add = function (term, field, metadata) {\n if (!(term in this.metadata)) {\n this.metadata[term] = Object.create(null)\n this.metadata[term][field] = metadata\n return\n }\n\n if (!(field in this.metadata[term])) {\n this.metadata[term][field] = metadata\n return\n }\n\n var metadataKeys = Object.keys(metadata)\n\n for (var i = 0; i < metadataKeys.length; i++) {\n var key = metadataKeys[i]\n\n if (key in this.metadata[term][field]) {\n this.metadata[term][field][key] = this.metadata[term][field][key].concat(metadata[key])\n } else {\n this.metadata[term][field][key] = metadata[key]\n }\n }\n}\n/**\n * A lunr.Query provides a programmatic way of defining queries to be performed\n * against a {@link lunr.Index}.\n *\n * Prefer constructing a lunr.Query using the {@link lunr.Index#query} method\n * so the query object is pre-initialized with the right index fields.\n *\n * @constructor\n * @property {lunr.Query~Clause[]} clauses - An array of query clauses.\n * @property {string[]} allFields - An array of all available fields in a lunr.Index.\n */\nlunr.Query = function (allFields) {\n this.clauses = []\n this.allFields = allFields\n}\n\n/**\n * Constants for indicating what kind of automatic wildcard insertion will be used when constructing a query clause.\n *\n * This allows wildcards to be added to the beginning and end of a term without having to manually do any string\n * concatenation.\n *\n * The wildcard constants can be bitwise combined to select both leading and trailing wildcards.\n *\n * @constant\n * @default\n * @property {number} wildcard.NONE - The term will have no wildcards inserted, this is the default behaviour\n * @property {number} wildcard.LEADING - Prepend the term with a wildcard, unless a leading wildcard already exists\n * @property {number} wildcard.TRAILING - Append a wildcard to the term, unless a trailing wildcard already exists\n * @see lunr.Query~Clause\n * @see lunr.Query#clause\n * @see lunr.Query#term\n * @example query term with trailing wildcard\n * query.term('foo', { wildcard: lunr.Query.wildcard.TRAILING })\n * @example query term with leading and trailing wildcard\n * query.term('foo', {\n * wildcard: lunr.Query.wildcard.LEADING | lunr.Query.wildcard.TRAILING\n * })\n */\n\nlunr.Query.wildcard = new String (\"*\")\nlunr.Query.wildcard.NONE = 0\nlunr.Query.wildcard.LEADING = 1\nlunr.Query.wildcard.TRAILING = 2\n\n/**\n * Constants for indicating what kind of presence a term must have in matching documents.\n *\n * @constant\n * @enum {number}\n * @see lunr.Query~Clause\n * @see lunr.Query#clause\n * @see lunr.Query#term\n * @example query term with required presence\n * query.term('foo', { presence: lunr.Query.presence.REQUIRED })\n */\nlunr.Query.presence = {\n /**\n * Term's presence in a document is optional, this is the default value.\n */\n OPTIONAL: 1,\n\n /**\n * Term's presence in a document is required, documents that do not contain\n * this term will not be returned.\n */\n REQUIRED: 2,\n\n /**\n * Term's presence in a document is prohibited, documents that do contain\n * this term will not be returned.\n */\n PROHIBITED: 3\n}\n\n/**\n * A single clause in a {@link lunr.Query} contains a term and details on how to\n * match that term against a {@link lunr.Index}.\n *\n * @typedef {Object} lunr.Query~Clause\n * @property {string[]} fields - The fields in an index this clause should be matched against.\n * @property {number} [boost=1] - Any boost that should be applied when matching this clause.\n * @property {number} [editDistance] - Whether the term should have fuzzy matching applied, and how fuzzy the match should be.\n * @property {boolean} [usePipeline] - Whether the term should be passed through the search pipeline.\n * @property {number} [wildcard=lunr.Query.wildcard.NONE] - Whether the term should have wildcards appended or prepended.\n * @property {number} [presence=lunr.Query.presence.OPTIONAL] - The terms presence in any matching documents.\n */\n\n/**\n * Adds a {@link lunr.Query~Clause} to this query.\n *\n * Unless the clause contains the fields to be matched all fields will be matched. In addition\n * a default boost of 1 is applied to the clause.\n *\n * @param {lunr.Query~Clause} clause - The clause to add to this query.\n * @see lunr.Query~Clause\n * @returns {lunr.Query}\n */\nlunr.Query.prototype.clause = function (clause) {\n if (!('fields' in clause)) {\n clause.fields = this.allFields\n }\n\n if (!('boost' in clause)) {\n clause.boost = 1\n }\n\n if (!('usePipeline' in clause)) {\n clause.usePipeline = true\n }\n\n if (!('wildcard' in clause)) {\n clause.wildcard = lunr.Query.wildcard.NONE\n }\n\n if ((clause.wildcard & lunr.Query.wildcard.LEADING) && (clause.term.charAt(0) != lunr.Query.wildcard)) {\n clause.term = \"*\" + clause.term\n }\n\n if ((clause.wildcard & lunr.Query.wildcard.TRAILING) && (clause.term.slice(-1) != lunr.Query.wildcard)) {\n clause.term = \"\" + clause.term + \"*\"\n }\n\n if (!('presence' in clause)) {\n clause.presence = lunr.Query.presence.OPTIONAL\n }\n\n this.clauses.push(clause)\n\n return this\n}\n\n/**\n * A negated query is one in which every clause has a presence of\n * prohibited. These queries require some special processing to return\n * the expected results.\n *\n * @returns boolean\n */\nlunr.Query.prototype.isNegated = function () {\n for (var i = 0; i < this.clauses.length; i++) {\n if (this.clauses[i].presence != lunr.Query.presence.PROHIBITED) {\n return false\n }\n }\n\n return true\n}\n\n/**\n * Adds a term to the current query, under the covers this will create a {@link lunr.Query~Clause}\n * to the list of clauses that make up this query.\n *\n * The term is used as is, i.e. no tokenization will be performed by this method. Instead conversion\n * to a token or token-like string should be done before calling this method.\n *\n * The term will be converted to a string by calling `toString`. Multiple terms can be passed as an\n * array, each term in the array will share the same options.\n *\n * @param {object|object[]} term - The term(s) to add to the query.\n * @param {object} [options] - Any additional properties to add to the query clause.\n * @returns {lunr.Query}\n * @see lunr.Query#clause\n * @see lunr.Query~Clause\n * @example adding a single term to a query\n * query.term(\"foo\")\n * @example adding a single term to a query and specifying search fields, term boost and automatic trailing wildcard\n * query.term(\"foo\", {\n * fields: [\"title\"],\n * boost: 10,\n * wildcard: lunr.Query.wildcard.TRAILING\n * })\n * @example using lunr.tokenizer to convert a string to tokens before using them as terms\n * query.term(lunr.tokenizer(\"foo bar\"))\n */\nlunr.Query.prototype.term = function (term, options) {\n if (Array.isArray(term)) {\n term.forEach(function (t) { this.term(t, lunr.utils.clone(options)) }, this)\n return this\n }\n\n var clause = options || {}\n clause.term = term.toString()\n\n this.clause(clause)\n\n return this\n}\nlunr.QueryParseError = function (message, start, end) {\n this.name = \"QueryParseError\"\n this.message = message\n this.start = start\n this.end = end\n}\n\nlunr.QueryParseError.prototype = new Error\nlunr.QueryLexer = function (str) {\n this.lexemes = []\n this.str = str\n this.length = str.length\n this.pos = 0\n this.start = 0\n this.escapeCharPositions = []\n}\n\nlunr.QueryLexer.prototype.run = function () {\n var state = lunr.QueryLexer.lexText\n\n while (state) {\n state = state(this)\n }\n}\n\nlunr.QueryLexer.prototype.sliceString = function () {\n var subSlices = [],\n sliceStart = this.start,\n sliceEnd = this.pos\n\n for (var i = 0; i < this.escapeCharPositions.length; i++) {\n sliceEnd = this.escapeCharPositions[i]\n subSlices.push(this.str.slice(sliceStart, sliceEnd))\n sliceStart = sliceEnd + 1\n }\n\n subSlices.push(this.str.slice(sliceStart, this.pos))\n this.escapeCharPositions.length = 0\n\n return subSlices.join('')\n}\n\nlunr.QueryLexer.prototype.emit = function (type) {\n this.lexemes.push({\n type: type,\n str: this.sliceString(),\n start: this.start,\n end: this.pos\n })\n\n this.start = this.pos\n}\n\nlunr.QueryLexer.prototype.escapeCharacter = function () {\n this.escapeCharPositions.push(this.pos - 1)\n this.pos += 1\n}\n\nlunr.QueryLexer.prototype.next = function () {\n if (this.pos >= this.length) {\n return lunr.QueryLexer.EOS\n }\n\n var char = this.str.charAt(this.pos)\n this.pos += 1\n return char\n}\n\nlunr.QueryLexer.prototype.width = function () {\n return this.pos - this.start\n}\n\nlunr.QueryLexer.prototype.ignore = function () {\n if (this.start == this.pos) {\n this.pos += 1\n }\n\n this.start = this.pos\n}\n\nlunr.QueryLexer.prototype.backup = function () {\n this.pos -= 1\n}\n\nlunr.QueryLexer.prototype.acceptDigitRun = function () {\n var char, charCode\n\n do {\n char = this.next()\n charCode = char.charCodeAt(0)\n } while (charCode > 47 && charCode < 58)\n\n if (char != lunr.QueryLexer.EOS) {\n this.backup()\n }\n}\n\nlunr.QueryLexer.prototype.more = function () {\n return this.pos < this.length\n}\n\nlunr.QueryLexer.EOS = 'EOS'\nlunr.QueryLexer.FIELD = 'FIELD'\nlunr.QueryLexer.TERM = 'TERM'\nlunr.QueryLexer.EDIT_DISTANCE = 'EDIT_DISTANCE'\nlunr.QueryLexer.BOOST = 'BOOST'\nlunr.QueryLexer.PRESENCE = 'PRESENCE'\n\nlunr.QueryLexer.lexField = function (lexer) {\n lexer.backup()\n lexer.emit(lunr.QueryLexer.FIELD)\n lexer.ignore()\n return lunr.QueryLexer.lexText\n}\n\nlunr.QueryLexer.lexTerm = function (lexer) {\n if (lexer.width() > 1) {\n lexer.backup()\n lexer.emit(lunr.QueryLexer.TERM)\n }\n\n lexer.ignore()\n\n if (lexer.more()) {\n return lunr.QueryLexer.lexText\n }\n}\n\nlunr.QueryLexer.lexEditDistance = function (lexer) {\n lexer.ignore()\n lexer.acceptDigitRun()\n lexer.emit(lunr.QueryLexer.EDIT_DISTANCE)\n return lunr.QueryLexer.lexText\n}\n\nlunr.QueryLexer.lexBoost = function (lexer) {\n lexer.ignore()\n lexer.acceptDigitRun()\n lexer.emit(lunr.QueryLexer.BOOST)\n return lunr.QueryLexer.lexText\n}\n\nlunr.QueryLexer.lexEOS = function (lexer) {\n if (lexer.width() > 0) {\n lexer.emit(lunr.QueryLexer.TERM)\n }\n}\n\n// This matches the separator used when tokenising fields\n// within a document. These should match otherwise it is\n// not possible to search for some tokens within a document.\n//\n// It is possible for the user to change the separator on the\n// tokenizer so it _might_ clash with any other of the special\n// characters already used within the search string, e.g. :.\n//\n// This means that it is possible to change the separator in\n// such a way that makes some words unsearchable using a search\n// string.\nlunr.QueryLexer.termSeparator = lunr.tokenizer.separator\n\nlunr.QueryLexer.lexText = function (lexer) {\n while (true) {\n var char = lexer.next()\n\n if (char == lunr.QueryLexer.EOS) {\n return lunr.QueryLexer.lexEOS\n }\n\n // Escape character is '\\'\n if (char.charCodeAt(0) == 92) {\n lexer.escapeCharacter()\n continue\n }\n\n if (char == \":\") {\n return lunr.QueryLexer.lexField\n }\n\n if (char == \"~\") {\n lexer.backup()\n if (lexer.width() > 0) {\n lexer.emit(lunr.QueryLexer.TERM)\n }\n return lunr.QueryLexer.lexEditDistance\n }\n\n if (char == \"^\") {\n lexer.backup()\n if (lexer.width() > 0) {\n lexer.emit(lunr.QueryLexer.TERM)\n }\n return lunr.QueryLexer.lexBoost\n }\n\n // \"+\" indicates term presence is required\n // checking for length to ensure that only\n // leading \"+\" are considered\n if (char == \"+\" && lexer.width() === 1) {\n lexer.emit(lunr.QueryLexer.PRESENCE)\n return lunr.QueryLexer.lexText\n }\n\n // \"-\" indicates term presence is prohibited\n // checking for length to ensure that only\n // leading \"-\" are considered\n if (char == \"-\" && lexer.width() === 1) {\n lexer.emit(lunr.QueryLexer.PRESENCE)\n return lunr.QueryLexer.lexText\n }\n\n if (char.match(lunr.QueryLexer.termSeparator)) {\n return lunr.QueryLexer.lexTerm\n }\n }\n}\n\nlunr.QueryParser = function (str, query) {\n this.lexer = new lunr.QueryLexer (str)\n this.query = query\n this.currentClause = {}\n this.lexemeIdx = 0\n}\n\nlunr.QueryParser.prototype.parse = function () {\n this.lexer.run()\n this.lexemes = this.lexer.lexemes\n\n var state = lunr.QueryParser.parseClause\n\n while (state) {\n state = state(this)\n }\n\n return this.query\n}\n\nlunr.QueryParser.prototype.peekLexeme = function () {\n return this.lexemes[this.lexemeIdx]\n}\n\nlunr.QueryParser.prototype.consumeLexeme = function () {\n var lexeme = this.peekLexeme()\n this.lexemeIdx += 1\n return lexeme\n}\n\nlunr.QueryParser.prototype.nextClause = function () {\n var completedClause = this.currentClause\n this.query.clause(completedClause)\n this.currentClause = {}\n}\n\nlunr.QueryParser.parseClause = function (parser) {\n var lexeme = parser.peekLexeme()\n\n if (lexeme == undefined) {\n return\n }\n\n switch (lexeme.type) {\n case lunr.QueryLexer.PRESENCE:\n return lunr.QueryParser.parsePresence\n case lunr.QueryLexer.FIELD:\n return lunr.QueryParser.parseField\n case lunr.QueryLexer.TERM:\n return lunr.QueryParser.parseTerm\n default:\n var errorMessage = \"expected either a field or a term, found \" + lexeme.type\n\n if (lexeme.str.length >= 1) {\n errorMessage += \" with value '\" + lexeme.str + \"'\"\n }\n\n throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end)\n }\n}\n\nlunr.QueryParser.parsePresence = function (parser) {\n var lexeme = parser.consumeLexeme()\n\n if (lexeme == undefined) {\n return\n }\n\n switch (lexeme.str) {\n case \"-\":\n parser.currentClause.presence = lunr.Query.presence.PROHIBITED\n break\n case \"+\":\n parser.currentClause.presence = lunr.Query.presence.REQUIRED\n break\n default:\n var errorMessage = \"unrecognised presence operator'\" + lexeme.str + \"'\"\n throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end)\n }\n\n var nextLexeme = parser.peekLexeme()\n\n if (nextLexeme == undefined) {\n var errorMessage = \"expecting term or field, found nothing\"\n throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end)\n }\n\n switch (nextLexeme.type) {\n case lunr.QueryLexer.FIELD:\n return lunr.QueryParser.parseField\n case lunr.QueryLexer.TERM:\n return lunr.QueryParser.parseTerm\n default:\n var errorMessage = \"expecting term or field, found '\" + nextLexeme.type + \"'\"\n throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end)\n }\n}\n\nlunr.QueryParser.parseField = function (parser) {\n var lexeme = parser.consumeLexeme()\n\n if (lexeme == undefined) {\n return\n }\n\n if (parser.query.allFields.indexOf(lexeme.str) == -1) {\n var possibleFields = parser.query.allFields.map(function (f) { return \"'\" + f + \"'\" }).join(', '),\n errorMessage = \"unrecognised field '\" + lexeme.str + \"', possible fields: \" + possibleFields\n\n throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end)\n }\n\n parser.currentClause.fields = [lexeme.str]\n\n var nextLexeme = parser.peekLexeme()\n\n if (nextLexeme == undefined) {\n var errorMessage = \"expecting term, found nothing\"\n throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end)\n }\n\n switch (nextLexeme.type) {\n case lunr.QueryLexer.TERM:\n return lunr.QueryParser.parseTerm\n default:\n var errorMessage = \"expecting term, found '\" + nextLexeme.type + \"'\"\n throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end)\n }\n}\n\nlunr.QueryParser.parseTerm = function (parser) {\n var lexeme = parser.consumeLexeme()\n\n if (lexeme == undefined) {\n return\n }\n\n parser.currentClause.term = lexeme.str.toLowerCase()\n\n if (lexeme.str.indexOf(\"*\") != -1) {\n parser.currentClause.usePipeline = false\n }\n\n var nextLexeme = parser.peekLexeme()\n\n if (nextLexeme == undefined) {\n parser.nextClause()\n return\n }\n\n switch (nextLexeme.type) {\n case lunr.QueryLexer.TERM:\n parser.nextClause()\n return lunr.QueryParser.parseTerm\n case lunr.QueryLexer.FIELD:\n parser.nextClause()\n return lunr.QueryParser.parseField\n case lunr.QueryLexer.EDIT_DISTANCE:\n return lunr.QueryParser.parseEditDistance\n case lunr.QueryLexer.BOOST:\n return lunr.QueryParser.parseBoost\n case lunr.QueryLexer.PRESENCE:\n parser.nextClause()\n return lunr.QueryParser.parsePresence\n default:\n var errorMessage = \"Unexpected lexeme type '\" + nextLexeme.type + \"'\"\n throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end)\n }\n}\n\nlunr.QueryParser.parseEditDistance = function (parser) {\n var lexeme = parser.consumeLexeme()\n\n if (lexeme == undefined) {\n return\n }\n\n var editDistance = parseInt(lexeme.str, 10)\n\n if (isNaN(editDistance)) {\n var errorMessage = \"edit distance must be numeric\"\n throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end)\n }\n\n parser.currentClause.editDistance = editDistance\n\n var nextLexeme = parser.peekLexeme()\n\n if (nextLexeme == undefined) {\n parser.nextClause()\n return\n }\n\n switch (nextLexeme.type) {\n case lunr.QueryLexer.TERM:\n parser.nextClause()\n return lunr.QueryParser.parseTerm\n case lunr.QueryLexer.FIELD:\n parser.nextClause()\n return lunr.QueryParser.parseField\n case lunr.QueryLexer.EDIT_DISTANCE:\n return lunr.QueryParser.parseEditDistance\n case lunr.QueryLexer.BOOST:\n return lunr.QueryParser.parseBoost\n case lunr.QueryLexer.PRESENCE:\n parser.nextClause()\n return lunr.QueryParser.parsePresence\n default:\n var errorMessage = \"Unexpected lexeme type '\" + nextLexeme.type + \"'\"\n throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end)\n }\n}\n\nlunr.QueryParser.parseBoost = function (parser) {\n var lexeme = parser.consumeLexeme()\n\n if (lexeme == undefined) {\n return\n }\n\n var boost = parseInt(lexeme.str, 10)\n\n if (isNaN(boost)) {\n var errorMessage = \"boost must be numeric\"\n throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end)\n }\n\n parser.currentClause.boost = boost\n\n var nextLexeme = parser.peekLexeme()\n\n if (nextLexeme == undefined) {\n parser.nextClause()\n return\n }\n\n switch (nextLexeme.type) {\n case lunr.QueryLexer.TERM:\n parser.nextClause()\n return lunr.QueryParser.parseTerm\n case lunr.QueryLexer.FIELD:\n parser.nextClause()\n return lunr.QueryParser.parseField\n case lunr.QueryLexer.EDIT_DISTANCE:\n return lunr.QueryParser.parseEditDistance\n case lunr.QueryLexer.BOOST:\n return lunr.QueryParser.parseBoost\n case lunr.QueryLexer.PRESENCE:\n parser.nextClause()\n return lunr.QueryParser.parsePresence\n default:\n var errorMessage = \"Unexpected lexeme type '\" + nextLexeme.type + \"'\"\n throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end)\n }\n}\n\n /**\n * export the module via AMD, CommonJS or as a browser global\n * Export code from https://github.com/umdjs/umd/blob/master/returnExports.js\n */\n ;(function (root, factory) {\n if (typeof define === 'function' && define.amd) {\n // AMD. Register as an anonymous module.\n define(factory)\n } else if (typeof exports === 'object') {\n /**\n * Node. Does not work with strict CommonJS, but\n * only CommonJS-like enviroments that support module.exports,\n * like Node.\n */\n module.exports = factory()\n } else {\n // Browser globals (root is window)\n root.lunr = factory()\n }\n }(this, function () {\n /**\n * Just return a value to define the module export.\n * This example returns an object, but the module\n * can return a function as the exported value.\n */\n return lunr\n }))\n})();\n", "/*!\n * escape-html\n * Copyright(c) 2012-2013 TJ Holowaychuk\n * Copyright(c) 2015 Andreas Lubbe\n * Copyright(c) 2015 Tiancheng \"Timothy\" Gu\n * MIT Licensed\n */\n\n'use strict';\n\n/**\n * Module variables.\n * @private\n */\n\nvar matchHtmlRegExp = /[\"'&<>]/;\n\n/**\n * Module exports.\n * @public\n */\n\nmodule.exports = escapeHtml;\n\n/**\n * Escape special characters in the given string of html.\n *\n * @param {string} string The string to escape for inserting into HTML\n * @return {string}\n * @public\n */\n\nfunction escapeHtml(string) {\n var str = '' + string;\n var match = matchHtmlRegExp.exec(str);\n\n if (!match) {\n return str;\n }\n\n var escape;\n var html = '';\n var index = 0;\n var lastIndex = 0;\n\n for (index = match.index; index < str.length; index++) {\n switch (str.charCodeAt(index)) {\n case 34: // \"\n escape = '"';\n break;\n case 38: // &\n escape = '&';\n break;\n case 39: // '\n escape = ''';\n break;\n case 60: // <\n escape = '<';\n break;\n case 62: // >\n escape = '>';\n break;\n default:\n continue;\n }\n\n if (lastIndex !== index) {\n html += str.substring(lastIndex, index);\n }\n\n lastIndex = index + 1;\n html += escape;\n }\n\n return lastIndex !== index\n ? html + str.substring(lastIndex, index)\n : html;\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A RTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport lunr from \"lunr\"\n\nimport \"~/polyfills\"\n\nimport { Search, SearchIndexConfig } from \"../../_\"\nimport {\n SearchMessage,\n SearchMessageType\n} from \"../message\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Add support for usage with `iframe-worker` polyfill\n *\n * While `importScripts` is synchronous when executed inside of a web worker,\n * it's not possible to provide a synchronous polyfilled implementation. The\n * cool thing is that awaiting a non-Promise is a noop, so extending the type\n * definition to return a `Promise` shouldn't break anything.\n *\n * @see https://bit.ly/2PjDnXi - GitHub comment\n */\ndeclare global {\n function importScripts(...urls: string[]): Promise | void\n}\n\n/* ----------------------------------------------------------------------------\n * Data\n * ------------------------------------------------------------------------- */\n\n/**\n * Search index\n */\nlet index: Search\n\n/* ----------------------------------------------------------------------------\n * Helper functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Fetch (= import) multi-language support through `lunr-languages`\n *\n * This function automatically imports the stemmers necessary to process the\n * languages, which are defined through the search index configuration.\n *\n * If the worker runs inside of an `iframe` (when using `iframe-worker` as\n * a shim), the base URL for the stemmers to be loaded must be determined by\n * searching for the first `script` element with a `src` attribute, which will\n * contain the contents of this script.\n *\n * @param config - Search index configuration\n *\n * @returns Promise resolving with no result\n */\nasync function setupSearchLanguages(\n config: SearchIndexConfig\n): Promise {\n let base = \"../lunr\"\n\n /* Detect `iframe-worker` and fix base URL */\n if (typeof parent !== \"undefined\" && \"IFrameWorker\" in parent) {\n const worker = document.querySelector(\"script[src]\")!\n const [path] = worker.src.split(\"/worker\")\n\n /* Prefix base with path */\n base = base.replace(\"..\", path)\n }\n\n /* Add scripts for languages */\n const scripts = []\n for (const lang of config.lang) {\n switch (lang) {\n\n /* Add segmenter for Japanese */\n case \"ja\":\n scripts.push(`${base}/tinyseg.js`)\n break\n\n /* Add segmenter for Hindi and Thai */\n case \"hi\":\n case \"th\":\n scripts.push(`${base}/wordcut.js`)\n break\n }\n\n /* Add language support */\n if (lang !== \"en\")\n scripts.push(`${base}/min/lunr.${lang}.min.js`)\n }\n\n /* Add multi-language support */\n if (config.lang.length > 1)\n scripts.push(`${base}/min/lunr.multi.min.js`)\n\n /* Load scripts synchronously */\n if (scripts.length)\n await importScripts(\n `${base}/min/lunr.stemmer.support.min.js`,\n ...scripts\n )\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Message handler\n *\n * @param message - Source message\n *\n * @returns Target message\n */\nexport async function handler(\n message: SearchMessage\n): Promise {\n switch (message.type) {\n\n /* Search setup message */\n case SearchMessageType.SETUP:\n await setupSearchLanguages(message.data.config)\n index = new Search(message.data)\n return {\n type: SearchMessageType.READY\n }\n\n /* Search query message */\n case SearchMessageType.QUERY:\n return {\n type: SearchMessageType.RESULT,\n data: index ? index.search(message.data) : { items: [] }\n }\n\n /* All other messages */\n default:\n throw new TypeError(\"Invalid message type\")\n }\n}\n\n/* ----------------------------------------------------------------------------\n * Worker\n * ------------------------------------------------------------------------- */\n\n/* @ts-expect-error - expose Lunr.js in global scope, or stemmers won't work */\nself.lunr = lunr\n\n/* Handle messages */\naddEventListener(\"message\", async ev => {\n postMessage(await handler(ev.data))\n})\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\n/* ----------------------------------------------------------------------------\n * Polyfills\n * ------------------------------------------------------------------------- */\n\n/* Polyfill `Object.entries` */\nif (!Object.entries)\n Object.entries = function (obj: object) {\n const data: [string, string][] = []\n for (const key of Object.keys(obj))\n // @ts-expect-error - ignore property access warning\n data.push([key, obj[key]])\n\n /* Return entries */\n return data\n }\n\n/* Polyfill `Object.values` */\nif (!Object.values)\n Object.values = function (obj: object) {\n const data: string[] = []\n for (const key of Object.keys(obj))\n // @ts-expect-error - ignore property access warning\n data.push(obj[key])\n\n /* Return values */\n return data\n }\n\n/* ------------------------------------------------------------------------- */\n\n/* Polyfills for `Element` */\nif (typeof Element !== \"undefined\") {\n\n /* Polyfill `Element.scrollTo` */\n if (!Element.prototype.scrollTo)\n Element.prototype.scrollTo = function (\n x?: ScrollToOptions | number, y?: number\n ): void {\n if (typeof x === \"object\") {\n this.scrollLeft = x.left!\n this.scrollTop = x.top!\n } else {\n this.scrollLeft = x!\n this.scrollTop = y!\n }\n }\n\n /* Polyfill `Element.replaceWith` */\n if (!Element.prototype.replaceWith)\n Element.prototype.replaceWith = function (\n ...nodes: Array\n ): void {\n const parent = this.parentNode\n if (parent) {\n if (nodes.length === 0)\n parent.removeChild(this)\n\n /* Replace children and create text nodes */\n for (let i = nodes.length - 1; i >= 0; i--) {\n let node = nodes[i]\n if (typeof node !== \"object\")\n node = document.createTextNode(node)\n else if (node.parentNode)\n node.parentNode.removeChild(node)\n\n /* Replace child or insert before previous sibling */\n if (!i)\n parent.replaceChild(node, this)\n else\n parent.insertBefore(this.previousSibling!, node)\n }\n }\n }\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport escapeHTML from \"escape-html\"\n\nimport { SearchIndexDocument } from \"../_\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Search document\n */\nexport interface SearchDocument extends SearchIndexDocument {\n parent?: SearchIndexDocument /* Parent article */\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Search document mapping\n */\nexport type SearchDocumentMap = Map\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Create a search document mapping\n *\n * @param docs - Search index documents\n *\n * @returns Search document map\n */\nexport function setupSearchDocumentMap(\n docs: SearchIndexDocument[]\n): SearchDocumentMap {\n const documents = new Map()\n const parents = new Set()\n for (const doc of docs) {\n const [path, hash] = doc.location.split(\"#\")\n\n /* Extract location, title and tags */\n const location = doc.location\n const title = doc.title\n const tags = doc.tags\n\n /* Escape and cleanup text */\n const text = escapeHTML(doc.text)\n .replace(/\\s+(?=[,.:;!?])/g, \"\")\n .replace(/\\s+/g, \" \")\n\n /* Handle section */\n if (hash) {\n const parent = documents.get(path)!\n\n /* Ignore first section, override article */\n if (!parents.has(parent)) {\n parent.title = doc.title\n parent.text = text\n\n /* Remember that we processed the article */\n parents.add(parent)\n\n /* Add subsequent section */\n } else {\n documents.set(location, {\n location,\n title,\n text,\n parent\n })\n }\n\n /* Add article */\n } else {\n documents.set(location, {\n location,\n title,\n text,\n ...tags && { tags }\n })\n }\n }\n return documents\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport escapeHTML from \"escape-html\"\n\nimport { SearchIndexConfig } from \"../_\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Search highlight function\n *\n * @param value - Value\n *\n * @returns Highlighted value\n */\nexport type SearchHighlightFn = (value: string) => string\n\n/**\n * Search highlight factory function\n *\n * @param query - Query value\n *\n * @returns Search highlight function\n */\nexport type SearchHighlightFactoryFn = (query: string) => SearchHighlightFn\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Create a search highlighter\n *\n * @param config - Search index configuration\n * @param escape - Whether to escape HTML\n *\n * @returns Search highlight factory function\n */\nexport function setupSearchHighlighter(\n config: SearchIndexConfig, escape: boolean\n): SearchHighlightFactoryFn {\n const separator = new RegExp(config.separator, \"img\")\n const highlight = (_: unknown, data: string, term: string) => {\n return `${data}${term}`\n }\n\n /* Return factory function */\n return (query: string) => {\n query = query\n .replace(/[\\s*+\\-:~^]+/g, \" \")\n .trim()\n\n /* Create search term match expression */\n const match = new RegExp(`(^|${config.separator})(${\n query\n .replace(/[|\\\\{}()[\\]^$+*?.-]/g, \"\\\\$&\")\n .replace(separator, \"|\")\n })`, \"img\")\n\n /* Highlight string value */\n return value => (\n escape\n ? escapeHTML(value)\n : value\n )\n .replace(match, highlight)\n .replace(/<\\/mark>(\\s+)]*>/img, \"$1\")\n }\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Search query clause\n */\nexport interface SearchQueryClause {\n presence: lunr.Query.presence /* Clause presence */\n term: string /* Clause term */\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Search query terms\n */\nexport type SearchQueryTerms = Record\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Parse a search query for analysis\n *\n * @param value - Query value\n *\n * @returns Search query clauses\n */\nexport function parseSearchQuery(\n value: string\n): SearchQueryClause[] {\n const query = new (lunr as any).Query([\"title\", \"text\"])\n const parser = new (lunr as any).QueryParser(value, query)\n\n /* Parse and return query clauses */\n parser.parse()\n return query.clauses\n}\n\n/**\n * Analyze the search query clauses in regard to the search terms found\n *\n * @param query - Search query clauses\n * @param terms - Search terms\n *\n * @returns Search query terms\n */\nexport function getSearchQueryTerms(\n query: SearchQueryClause[], terms: string[]\n): SearchQueryTerms {\n const clauses = new Set(query)\n\n /* Match query clauses against terms */\n const result: SearchQueryTerms = {}\n for (let t = 0; t < terms.length; t++)\n for (const clause of clauses)\n if (terms[t].startsWith(clause.term)) {\n result[clause.term] = true\n clauses.delete(clause)\n }\n\n /* Annotate unmatched non-stopword query clauses */\n for (const clause of clauses)\n if (lunr.stopWordFilter?.(clause.term as any))\n result[clause.term] = false\n\n /* Return query terms */\n return result\n}\n", "/*\n * Copyright (c) 2016-2022 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n SearchDocument,\n SearchDocumentMap,\n setupSearchDocumentMap\n} from \"../document\"\nimport {\n SearchHighlightFactoryFn,\n setupSearchHighlighter\n} from \"../highlighter\"\nimport { SearchOptions } from \"../options\"\nimport {\n SearchQueryTerms,\n getSearchQueryTerms,\n parseSearchQuery\n} from \"../query\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Search index configuration\n */\nexport interface SearchIndexConfig {\n lang: string[] /* Search languages */\n separator: string /* Search separator */\n}\n\n/**\n * Search index document\n */\nexport interface SearchIndexDocument {\n location: string /* Document location */\n title: string /* Document title */\n text: string /* Document text */\n tags?: string[] /* Document tags */\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Search index\n *\n * This interfaces describes the format of the `search_index.json` file which\n * is automatically built by the MkDocs search plugin.\n */\nexport interface SearchIndex {\n config: SearchIndexConfig /* Search index configuration */\n docs: SearchIndexDocument[] /* Search index documents */\n options: SearchOptions /* Search options */\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Search metadata\n */\nexport interface SearchMetadata {\n score: number /* Score (relevance) */\n terms: SearchQueryTerms /* Search query terms */\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Search result document\n */\nexport type SearchResultDocument = SearchDocument & SearchMetadata\n\n/**\n * Search result item\n */\nexport type SearchResultItem = SearchResultDocument[]\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Search result\n */\nexport interface SearchResult {\n items: SearchResultItem[] /* Search result items */\n suggestions?: string[] /* Search suggestions */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Compute the difference of two lists of strings\n *\n * @param a - 1st list of strings\n * @param b - 2nd list of strings\n *\n * @returns Difference\n */\nfunction difference(a: string[], b: string[]): string[] {\n const [x, y] = [new Set(a), new Set(b)]\n return [\n ...new Set([...x].filter(value => !y.has(value)))\n ]\n}\n\n/* ----------------------------------------------------------------------------\n * Class\n * ------------------------------------------------------------------------- */\n\n/**\n * Search index\n */\nexport class Search {\n\n /**\n * Search document mapping\n *\n * A mapping of URLs (including hash fragments) to the actual articles and\n * sections of the documentation. The search document mapping must be created\n * regardless of whether the index was prebuilt or not, as Lunr.js itself\n * only stores the actual index.\n */\n protected documents: SearchDocumentMap\n\n /**\n * Search highlight factory function\n */\n protected highlight: SearchHighlightFactoryFn\n\n /**\n * The underlying Lunr.js search index\n */\n protected index: lunr.Index\n\n /**\n * Search options\n */\n protected options: SearchOptions\n\n /**\n * Create the search integration\n *\n * @param data - Search index\n */\n public constructor({ config, docs, options }: SearchIndex) {\n this.options = options\n\n /* Set up document map and highlighter factory */\n this.documents = setupSearchDocumentMap(docs)\n this.highlight = setupSearchHighlighter(config, false)\n\n /* Set separator for tokenizer */\n lunr.tokenizer.separator = new RegExp(config.separator)\n\n /* Create search index */\n this.index = lunr(function () {\n\n /* Set up multi-language support */\n if (config.lang.length === 1 && config.lang[0] !== \"en\") {\n this.use((lunr as any)[config.lang[0]])\n } else if (config.lang.length > 1) {\n this.use((lunr as any).multiLanguage(...config.lang))\n }\n\n /* Compute functions to be removed from the pipeline */\n const fns = difference([\n \"trimmer\", \"stopWordFilter\", \"stemmer\"\n ], options.pipeline)\n\n /* Remove functions from the pipeline for registered languages */\n for (const lang of config.lang.map(language => (\n language === \"en\" ? lunr : (lunr as any)[language]\n ))) {\n for (const fn of fns) {\n this.pipeline.remove(lang[fn])\n this.searchPipeline.remove(lang[fn])\n }\n }\n\n /* Set up reference */\n this.ref(\"location\")\n\n /* Set up fields */\n this.field(\"title\", { boost: 1e3 })\n this.field(\"text\")\n this.field(\"tags\", { boost: 1e6 })\n\n /* Index documents */\n for (const doc of docs)\n this.add(doc)\n })\n }\n\n /**\n * Search for matching documents\n *\n * The search index which MkDocs provides is divided up into articles, which\n * contain the whole content of the individual pages, and sections, which only\n * contain the contents of the subsections obtained by breaking the individual\n * pages up at `h1` ... `h6`. As there may be many sections on different pages\n * with identical titles (for example within this very project, e.g. \"Usage\"\n * or \"Installation\"), they need to be put into the context of the containing\n * page. For this reason, section results are grouped within their respective\n * articles which are the top-level results that are returned.\n *\n * @param query - Query value\n *\n * @returns Search results\n */\n public search(query: string): SearchResult {\n if (query) {\n try {\n const highlight = this.highlight(query)\n\n /* Parse query to extract clauses for analysis */\n const clauses = parseSearchQuery(query)\n .filter(clause => (\n clause.presence !== lunr.Query.presence.PROHIBITED\n ))\n\n /* Perform search and post-process results */\n const groups = this.index.search(`${query}*`)\n\n /* Apply post-query boosts based on title and search query terms */\n .reduce((item, { ref, score, matchData }) => {\n const document = this.documents.get(ref)\n if (typeof document !== \"undefined\") {\n const { location, title, text, tags, parent } = document\n\n /* Compute and analyze search query terms */\n const terms = getSearchQueryTerms(\n clauses,\n Object.keys(matchData.metadata)\n )\n\n /* Highlight title and text and apply post-query boosts */\n const boost = +!parent + +Object.values(terms).every(t => t)\n item.push({\n location,\n title: highlight(title),\n text: highlight(text),\n ...tags && { tags: tags.map(highlight) },\n score: score * (1 + boost),\n terms\n })\n }\n return item\n }, [])\n\n /* Sort search results again after applying boosts */\n .sort((a, b) => b.score - a.score)\n\n /* Group search results by page */\n .reduce((items, result) => {\n const document = this.documents.get(result.location)\n if (typeof document !== \"undefined\") {\n const ref = \"parent\" in document\n ? document.parent!.location\n : document.location\n items.set(ref, [...items.get(ref) || [], result])\n }\n return items\n }, new Map())\n\n /* Generate search suggestions, if desired */\n let suggestions: string[] | undefined\n if (this.options.suggestions) {\n const titles = this.index.query(builder => {\n for (const clause of clauses)\n builder.term(clause.term, {\n fields: [\"title\"],\n presence: lunr.Query.presence.REQUIRED,\n wildcard: lunr.Query.wildcard.TRAILING\n })\n })\n\n /* Retrieve suggestions for best match */\n suggestions = titles.length\n ? Object.keys(titles[0].matchData.metadata)\n : []\n }\n\n /* Return items and suggestions */\n return {\n items: [...groups.values()],\n ...typeof suggestions !== \"undefined\" && { suggestions }\n }\n\n /* Log errors to console (for now) */\n } catch {\n console.warn(`Invalid query: ${query} \u2013 see https://bit.ly/2s3ChXG`)\n }\n }\n\n /* Return nothing in case of error or empty query */\n return { items: [] }\n }\n}\n"], + "mappings": "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", + "names": [] +} diff --git a/v0.13.6/assets/stylesheets/main.644de097.min.css b/v0.13.6/assets/stylesheets/main.644de097.min.css new file mode 100644 index 0000000000..c7462620ac --- /dev/null +++ b/v0.13.6/assets/stylesheets/main.644de097.min.css @@ -0,0 +1 @@ +@charset "UTF-8";html{-webkit-text-size-adjust:none;-moz-text-size-adjust:none;-ms-text-size-adjust:none;text-size-adjust:none;box-sizing:border-box}*,:after,:before{box-sizing:inherit}@media (prefers-reduced-motion){*,:after,:before{transition:none!important}}body{margin:0}a,button,input,label{-webkit-tap-highlight-color:transparent}a{color:inherit;text-decoration:none}hr{border:0;box-sizing:initial;display:block;height:.05rem;overflow:visible;padding:0}small{font-size:80%}sub,sup{line-height:1em}img{border-style:none}table{border-collapse:initial;border-spacing:0}td,th{font-weight:400;vertical-align:top}button{background:transparent;border:0;font-family:inherit;font-size:inherit;margin:0;padding:0}input{border:0;outline:none}:root{--md-default-fg-color:rgba(0,0,0,.87);--md-default-fg-color--light:rgba(0,0,0,.54);--md-default-fg-color--lighter:rgba(0,0,0,.32);--md-default-fg-color--lightest:rgba(0,0,0,.07);--md-default-bg-color:#fff;--md-default-bg-color--light:hsla(0,0%,100%,.7);--md-default-bg-color--lighter:hsla(0,0%,100%,.3);--md-default-bg-color--lightest:hsla(0,0%,100%,.12);--md-primary-fg-color:#4051b5;--md-primary-fg-color--light:#5d6cc0;--md-primary-fg-color--dark:#303fa1;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,.7);--md-accent-fg-color:#526cfe;--md-accent-fg-color--transparent:rgba(82,108,254,.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,.7);--md-shadow-z1:0 0.2rem 0.5rem rgba(0,0,0,.05),0 0 0.05rem rgba(0,0,0,.1);--md-shadow-z2:0 0.2rem 0.5rem rgba(0,0,0,.1),0 0 0.05rem rgba(0,0,0,.25);--md-shadow-z3:0 0.2rem 0.5rem rgba(0,0,0,.2),0 0 0.05rem rgba(0,0,0,.35)}:root>*{--md-code-fg-color:#36464e;--md-code-bg-color:#f5f5f5;--md-code-hl-color:rgba(255,255,0,.5);--md-code-hl-number-color:#d52a2a;--md-code-hl-special-color:#db1457;--md-code-hl-function-color:#a846b9;--md-code-hl-constant-color:#6e59d9;--md-code-hl-keyword-color:#3f6ec6;--md-code-hl-string-color:#1c7d4d;--md-code-hl-name-color:var(--md-code-fg-color);--md-code-hl-operator-color:var(--md-default-fg-color--light);--md-code-hl-punctuation-color:var(--md-default-fg-color--light);--md-code-hl-comment-color:var(--md-default-fg-color--light);--md-code-hl-generic-color:var(--md-default-fg-color--light);--md-code-hl-variable-color:var(--md-default-fg-color--light);--md-typeset-color:var(--md-default-fg-color);--md-typeset-a-color:var(--md-primary-fg-color);--md-typeset-mark-color:rgba(255,255,0,.5);--md-typeset-del-color:rgba(245,80,61,.15);--md-typeset-ins-color:rgba(11,213,112,.15);--md-typeset-kbd-color:#fafafa;--md-typeset-kbd-accent-color:#fff;--md-typeset-kbd-border-color:#b8b8b8;--md-typeset-table-color:rgba(0,0,0,.12);--md-admonition-fg-color:var(--md-default-fg-color);--md-admonition-bg-color:var(--md-default-bg-color);--md-footer-fg-color:#fff;--md-footer-fg-color--light:hsla(0,0%,100%,.7);--md-footer-fg-color--lighter:hsla(0,0%,100%,.3);--md-footer-bg-color:rgba(0,0,0,.87);--md-footer-bg-color--dark:rgba(0,0,0,.32)}.md-icon svg{fill:currentcolor;display:block;height:1.2rem;width:1.2rem}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;--md-text-font-family:var(--md-text-font,_),-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif;--md-code-font-family:var(--md-code-font,_),SFMono-Regular,Consolas,Menlo,monospace}body,input{font-feature-settings:"kern","liga";font-family:var(--md-text-font-family)}body,code,input,kbd,pre{color:var(--md-typeset-color)}code,kbd,pre{font-feature-settings:"kern";font-family:var(--md-code-font-family)}:root{--md-typeset-table-sort-icon:url('data:image/svg+xml;charset=utf-8,');--md-typeset-table-sort-icon--asc:url('data:image/svg+xml;charset=utf-8,');--md-typeset-table-sort-icon--desc:url('data:image/svg+xml;charset=utf-8,')}.md-typeset{-webkit-print-color-adjust:exact;color-adjust:exact;font-size:.8rem;line-height:1.6}@media print{.md-typeset{font-size:.68rem}}.md-typeset blockquote,.md-typeset dl,.md-typeset figure,.md-typeset ol,.md-typeset pre,.md-typeset ul{margin-bottom:1em;margin-top:1em}.md-typeset h1{color:var(--md-default-fg-color--light);font-size:2em;line-height:1.3;margin:0 0 1.25em}.md-typeset h1,.md-typeset h2{font-weight:300;letter-spacing:-.01em}.md-typeset h2{font-size:1.5625em;line-height:1.4;margin:1.6em 0 .64em}.md-typeset h3{font-size:1.25em;font-weight:400;letter-spacing:-.01em;line-height:1.5;margin:1.6em 0 .8em}.md-typeset h2+h3{margin-top:.8em}.md-typeset h4{font-weight:700;letter-spacing:-.01em;margin:1em 0}.md-typeset h5,.md-typeset h6{color:var(--md-default-fg-color--light);font-size:.8em;font-weight:700;letter-spacing:-.01em;margin:1.25em 0}.md-typeset h5{text-transform:uppercase}.md-typeset hr{border-bottom:.05rem solid var(--md-default-fg-color--lightest);display:flow-root;margin:1.5em 0}.md-typeset a{color:var(--md-typeset-a-color);word-break:break-word}.md-typeset a,.md-typeset a:before{transition:color 125ms}.md-typeset a:focus,.md-typeset a:hover{color:var(--md-accent-fg-color)}.md-typeset a:focus code,.md-typeset a:hover code{background-color:var(--md-accent-fg-color--transparent)}.md-typeset a code{color:currentcolor}.md-typeset a.focus-visible{outline-color:var(--md-accent-fg-color);outline-offset:.2rem}.md-typeset code,.md-typeset kbd,.md-typeset pre{color:var(--md-code-fg-color);direction:ltr}@media print{.md-typeset code,.md-typeset kbd,.md-typeset pre{white-space:pre-wrap}}.md-typeset code{background-color:var(--md-code-bg-color);border-radius:.1rem;-webkit-box-decoration-break:clone;box-decoration-break:clone;font-size:.85em;padding:0 .2941176471em;transition:background-color 125ms;word-break:break-word}.md-typeset code:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}.md-typeset pre{display:flow-root;line-height:1.4;position:relative}.md-typeset pre>code{-webkit-box-decoration-break:slice;box-decoration-break:slice;box-shadow:none;display:block;margin:0;outline-color:var(--md-accent-fg-color);overflow:auto;padding:.7720588235em 1.1764705882em;scrollbar-color:var(--md-default-fg-color--lighter) transparent;scrollbar-width:thin;touch-action:auto;word-break:normal}.md-typeset pre>code:hover{scrollbar-color:var(--md-accent-fg-color) transparent}.md-typeset pre>code::-webkit-scrollbar{height:.2rem;width:.2rem}.md-typeset pre>code::-webkit-scrollbar-thumb{background-color:var(--md-default-fg-color--lighter)}.md-typeset pre>code::-webkit-scrollbar-thumb:hover{background-color:var(--md-accent-fg-color)}.md-typeset kbd{background-color:var(--md-typeset-kbd-color);border-radius:.1rem;box-shadow:0 .1rem 0 .05rem var(--md-typeset-kbd-border-color),0 .1rem 0 var(--md-typeset-kbd-border-color),0 -.1rem .2rem var(--md-typeset-kbd-accent-color) inset;color:var(--md-default-fg-color);display:inline-block;font-size:.75em;padding:0 .6666666667em;vertical-align:text-top;word-break:break-word}.md-typeset mark{background-color:var(--md-typeset-mark-color);-webkit-box-decoration-break:clone;box-decoration-break:clone;color:inherit;word-break:break-word}.md-typeset abbr{border-bottom:.05rem dotted var(--md-default-fg-color--light);cursor:help;text-decoration:none}@media (hover:none){.md-typeset abbr{position:relative}.md-typeset abbr[title]:-webkit-any(:focus,:hover):after{background-color:var(--md-default-fg-color);border-radius:.1rem;box-shadow:var(--md-shadow-z3);color:var(--md-default-bg-color);content:attr(title);display:inline-block;font-size:.7rem;margin-top:2em;max-width:80%;min-width:-webkit-max-content;min-width:max-content;padding:.2rem .3rem;position:absolute;width:auto}.md-typeset abbr[title]:-moz-any(:focus,:hover):after{background-color:var(--md-default-fg-color);border-radius:.1rem;box-shadow:var(--md-shadow-z3);color:var(--md-default-bg-color);content:attr(title);display:inline-block;font-size:.7rem;margin-top:2em;max-width:80%;min-width:-moz-max-content;min-width:max-content;padding:.2rem .3rem;position:absolute;width:auto}[dir=ltr] .md-typeset abbr[title]:-webkit-any(:focus,:hover):after{left:0}[dir=ltr] .md-typeset abbr[title]:-moz-any(:focus,:hover):after{left:0}[dir=ltr] .md-typeset abbr[title]:is(:focus,:hover):after{left:0}[dir=rtl] .md-typeset abbr[title]:-webkit-any(:focus,:hover):after{right:0}[dir=rtl] .md-typeset abbr[title]:-moz-any(:focus,:hover):after{right:0}[dir=rtl] .md-typeset abbr[title]:is(:focus,:hover):after{right:0}.md-typeset abbr[title]:is(:focus,:hover):after{background-color:var(--md-default-fg-color);border-radius:.1rem;box-shadow:var(--md-shadow-z3);color:var(--md-default-bg-color);content:attr(title);display:inline-block;font-size:.7rem;margin-top:2em;max-width:80%;min-width:-webkit-max-content;min-width:-moz-max-content;min-width:max-content;padding:.2rem .3rem;position:absolute;width:auto}}.md-typeset small{opacity:.75}[dir=ltr] .md-typeset sub,[dir=ltr] .md-typeset sup{margin-left:.078125em}[dir=rtl] .md-typeset sub,[dir=rtl] .md-typeset sup{margin-right:.078125em}[dir=ltr] .md-typeset blockquote{padding-left:.6rem}[dir=rtl] .md-typeset blockquote{padding-right:.6rem}[dir=ltr] .md-typeset blockquote{border-left:.2rem solid var(--md-default-fg-color--lighter)}[dir=rtl] .md-typeset blockquote{border-right:.2rem solid var(--md-default-fg-color--lighter)}.md-typeset blockquote{color:var(--md-default-fg-color--light);margin-left:0;margin-right:0}.md-typeset ul{list-style-type:disc}[dir=ltr] .md-typeset ol,[dir=ltr] .md-typeset ul{margin-left:.625em}[dir=rtl] .md-typeset ol,[dir=rtl] .md-typeset ul{margin-right:.625em}.md-typeset ol,.md-typeset ul{padding:0}.md-typeset ol:not([hidden]),.md-typeset ul:not([hidden]){display:flow-root}.md-typeset ol ol,.md-typeset ul ol{list-style-type:lower-alpha}.md-typeset ol ol ol,.md-typeset ul ol ol{list-style-type:lower-roman}[dir=ltr] .md-typeset ol li,[dir=ltr] .md-typeset ul li{margin-left:1.25em}[dir=rtl] .md-typeset ol li,[dir=rtl] .md-typeset ul li{margin-right:1.25em}.md-typeset ol li,.md-typeset ul li{margin-bottom:.5em}.md-typeset ol li blockquote,.md-typeset ol li p,.md-typeset ul li blockquote,.md-typeset ul li p{margin:.5em 0}.md-typeset ol li:last-child,.md-typeset ul li:last-child{margin-bottom:0}.md-typeset ol li :-webkit-any(ul,ol),.md-typeset ul li :-webkit-any(ul,ol){margin-bottom:.5em;margin-top:.5em}.md-typeset ol li :-moz-any(ul,ol),.md-typeset ul li :-moz-any(ul,ol){margin-bottom:.5em;margin-top:.5em}[dir=ltr] .md-typeset ol li :-webkit-any(ul,ol),[dir=ltr] .md-typeset ul li :-webkit-any(ul,ol){margin-left:.625em}[dir=ltr] .md-typeset ol li :-moz-any(ul,ol),[dir=ltr] .md-typeset ul li :-moz-any(ul,ol){margin-left:.625em}[dir=ltr] .md-typeset ol li :is(ul,ol),[dir=ltr] .md-typeset ul li :is(ul,ol){margin-left:.625em}[dir=rtl] .md-typeset ol li :-webkit-any(ul,ol),[dir=rtl] .md-typeset ul li :-webkit-any(ul,ol){margin-right:.625em}[dir=rtl] .md-typeset ol li :-moz-any(ul,ol),[dir=rtl] .md-typeset ul li :-moz-any(ul,ol){margin-right:.625em}[dir=rtl] .md-typeset ol li :is(ul,ol),[dir=rtl] .md-typeset ul li :is(ul,ol){margin-right:.625em}.md-typeset ol li :is(ul,ol),.md-typeset ul li :is(ul,ol){margin-bottom:.5em;margin-top:.5em}[dir=ltr] .md-typeset dd{margin-left:1.875em}[dir=rtl] .md-typeset dd{margin-right:1.875em}.md-typeset dd{margin-bottom:1.5em;margin-top:1em}.md-typeset img,.md-typeset svg{height:auto;max-width:100%}.md-typeset img[align=left],.md-typeset svg[align=left]{margin:1em 1em 1em 0}.md-typeset img[align=right],.md-typeset svg[align=right]{margin:1em 0 1em 1em}.md-typeset img[align]:only-child,.md-typeset svg[align]:only-child{margin-top:0}.md-typeset img[src$="#only-dark"]{display:none}.md-typeset figure{display:flow-root;margin:1em auto;max-width:100%;text-align:center;width:-webkit-fit-content;width:-moz-fit-content;width:fit-content}.md-typeset figure img{display:block}.md-typeset figcaption{font-style:italic;margin:1em auto;max-width:24rem}.md-typeset iframe{max-width:100%}.md-typeset table:not([class]){background-color:var(--md-default-bg-color);border:.05rem solid var(--md-typeset-table-color);border-radius:.1rem;display:inline-block;font-size:.64rem;max-width:100%;overflow:auto;touch-action:auto}@media print{.md-typeset table:not([class]){display:table}}.md-typeset table:not([class])+*{margin-top:1.5em}.md-typeset table:not([class]) :-webkit-any(th,td)>:first-child{margin-top:0}.md-typeset table:not([class]) :-moz-any(th,td)>:first-child{margin-top:0}.md-typeset table:not([class]) :is(th,td)>:first-child{margin-top:0}.md-typeset table:not([class]) :-webkit-any(th,td)>:last-child{margin-bottom:0}.md-typeset table:not([class]) :-moz-any(th,td)>:last-child{margin-bottom:0}.md-typeset table:not([class]) :is(th,td)>:last-child{margin-bottom:0}.md-typeset table:not([class]) :-webkit-any(th,td):not([align]){text-align:left}.md-typeset table:not([class]) :-moz-any(th,td):not([align]){text-align:left}.md-typeset table:not([class]) :is(th,td):not([align]){text-align:left}[dir=rtl] .md-typeset table:not([class]) :-webkit-any(th,td):not([align]){text-align:right}[dir=rtl] .md-typeset table:not([class]) :-moz-any(th,td):not([align]){text-align:right}[dir=rtl] .md-typeset table:not([class]) :is(th,td):not([align]){text-align:right}.md-typeset table:not([class]) th{font-weight:700;min-width:5rem;padding:.9375em 1.25em;vertical-align:top}.md-typeset table:not([class]) th a{color:inherit}.md-typeset table:not([class]) td{border-top:.05rem solid var(--md-typeset-table-color);padding:.9375em 1.25em;vertical-align:top}.md-typeset table:not([class]) tbody tr{transition:background-color 125ms}.md-typeset table:not([class]) tbody tr:hover{background-color:rgba(0,0,0,.035);box-shadow:0 .05rem 0 var(--md-default-bg-color) inset}.md-typeset table:not([class]) a{word-break:normal}.md-typeset table th[role=columnheader]{cursor:pointer}[dir=ltr] .md-typeset table th[role=columnheader]:after{margin-left:.5em}[dir=rtl] .md-typeset table th[role=columnheader]:after{margin-right:.5em}.md-typeset table th[role=columnheader]:after{content:"";display:inline-block;height:1.2em;-webkit-mask-image:var(--md-typeset-table-sort-icon);mask-image:var(--md-typeset-table-sort-icon);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;transition:background-color 125ms;vertical-align:text-bottom;width:1.2em}.md-typeset table th[role=columnheader]:hover:after{background-color:var(--md-default-fg-color--lighter)}.md-typeset table th[role=columnheader][aria-sort=ascending]:after{background-color:var(--md-default-fg-color--light);-webkit-mask-image:var(--md-typeset-table-sort-icon--asc);mask-image:var(--md-typeset-table-sort-icon--asc)}.md-typeset table th[role=columnheader][aria-sort=descending]:after{background-color:var(--md-default-fg-color--light);-webkit-mask-image:var(--md-typeset-table-sort-icon--desc);mask-image:var(--md-typeset-table-sort-icon--desc)}.md-typeset__scrollwrap{margin:1em -.8rem;overflow-x:auto;touch-action:auto}.md-typeset__table{display:inline-block;margin-bottom:.5em;padding:0 .8rem}@media print{.md-typeset__table{display:block}}html .md-typeset__table table{display:table;margin:0;overflow:hidden;width:100%}@media screen and (max-width:44.9375em){.md-content__inner>pre{margin:1em -.8rem}.md-content__inner>pre code{border-radius:0}}.md-banner{background-color:var(--md-footer-bg-color);color:var(--md-footer-fg-color);overflow:auto}@media print{.md-banner{display:none}}.md-banner--warning{background:var(--md-typeset-mark-color);color:var(--md-default-fg-color)}.md-banner__inner{font-size:.7rem;margin:.6rem auto;padding:0 .8rem}html{font-size:125%;height:100%;overflow-x:hidden}@media screen and (min-width:100em){html{font-size:137.5%}}@media screen and (min-width:125em){html{font-size:150%}}body{background-color:var(--md-default-bg-color);display:flex;flex-direction:column;font-size:.5rem;min-height:100%;position:relative;width:100%}@media print{body{display:block}}@media screen and (max-width:59.9375em){body[data-md-state=lock]{position:fixed}}.md-grid{margin-left:auto;margin-right:auto;max-width:61rem}.md-container{display:flex;flex-direction:column;flex-grow:1}@media print{.md-container{display:block}}.md-main{flex-grow:1}.md-main__inner{display:flex;height:100%;margin-top:1.5rem}.md-ellipsis{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.md-toggle{display:none}.md-option{height:0;opacity:0;position:absolute;width:0}.md-option:checked+label:not([hidden]){display:block}.md-option.focus-visible+label{outline-color:var(--md-accent-fg-color);outline-style:auto}.md-skip{background-color:var(--md-default-fg-color);border-radius:.1rem;color:var(--md-default-bg-color);font-size:.64rem;margin:.5rem;opacity:0;outline-color:var(--md-accent-fg-color);padding:.3rem .5rem;position:fixed;transform:translateY(.4rem);z-index:-1}.md-skip:focus{opacity:1;transform:translateY(0);transition:transform .25s cubic-bezier(.4,0,.2,1),opacity 175ms 75ms;z-index:10}@page{margin:25mm}:root{--md-clipboard-icon:url('data:image/svg+xml;charset=utf-8,')}.md-clipboard{border-radius:.1rem;color:var(--md-default-fg-color--lightest);cursor:pointer;height:1.5em;outline-color:var(--md-accent-fg-color);outline-offset:.1rem;position:absolute;right:.5em;top:.5em;transition:color .25s;width:1.5em;z-index:1}@media print{.md-clipboard{display:none}}.md-clipboard:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}:hover>.md-clipboard{color:var(--md-default-fg-color--light)}.md-clipboard:-webkit-any(:focus,:hover){color:var(--md-accent-fg-color)}.md-clipboard:-moz-any(:focus,:hover){color:var(--md-accent-fg-color)}.md-clipboard:is(:focus,:hover){color:var(--md-accent-fg-color)}.md-clipboard:after{background-color:currentcolor;content:"";display:block;height:1.125em;margin:0 auto;-webkit-mask-image:var(--md-clipboard-icon);mask-image:var(--md-clipboard-icon);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:1.125em}.md-clipboard--inline{cursor:pointer}.md-clipboard--inline code{transition:color .25s,background-color .25s}.md-clipboard--inline:-webkit-any(:focus,:hover) code{background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}.md-clipboard--inline:-moz-any(:focus,:hover) code{background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}.md-clipboard--inline:is(:focus,:hover) code{background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}.md-content{flex-grow:1;min-width:0}.md-content__inner{margin:0 .8rem 1.2rem;padding-top:.6rem}@media screen and (min-width:76.25em){[dir=ltr] .md-sidebar--primary:not([hidden])~.md-content>.md-content__inner{margin-left:1.2rem}[dir=ltr] .md-sidebar--secondary:not([hidden])~.md-content>.md-content__inner,[dir=rtl] .md-sidebar--primary:not([hidden])~.md-content>.md-content__inner{margin-right:1.2rem}[dir=rtl] .md-sidebar--secondary:not([hidden])~.md-content>.md-content__inner{margin-left:1.2rem}}.md-content__inner:before{content:"";display:block;height:.4rem}.md-content__inner>:last-child{margin-bottom:0}[dir=ltr] .md-content__button{margin-left:.4rem}[dir=rtl] .md-content__button{margin-right:.4rem}.md-content__button{float:right;margin:.4rem 0;padding:0}@media print{.md-content__button{display:none}}[dir=rtl] .md-content__button{float:left}.md-typeset .md-content__button{color:var(--md-default-fg-color--lighter)}.md-content__button svg{display:inline;vertical-align:top}[dir=rtl] .md-content__button svg{transform:scaleX(-1)}[dir=ltr] .md-dialog{right:.8rem}[dir=rtl] .md-dialog{left:.8rem}.md-dialog{background-color:var(--md-default-fg-color);border-radius:.1rem;bottom:.8rem;box-shadow:var(--md-shadow-z3);min-width:11.1rem;opacity:0;padding:.4rem .6rem;pointer-events:none;position:fixed;transform:translateY(100%);transition:transform 0ms .4s,opacity .4s;z-index:4}@media print{.md-dialog{display:none}}.md-dialog[data-md-state=open]{opacity:1;pointer-events:auto;transform:translateY(0);transition:transform .4s cubic-bezier(.075,.85,.175,1),opacity .4s}.md-dialog__inner{color:var(--md-default-bg-color);font-size:.7rem}.md-footer{background-color:var(--md-footer-bg-color);color:var(--md-footer-fg-color)}@media print{.md-footer{display:none}}.md-footer__inner{display:flex;justify-content:space-between;overflow:auto;padding:.2rem}.md-footer__link{display:flex;flex-grow:0.01;outline-color:var(--md-accent-fg-color);overflow:hidden;padding-bottom:.4rem;padding-top:1.4rem;transition:opacity .25s}.md-footer__link:-webkit-any(:focus,:hover){opacity:.7}.md-footer__link:-moz-any(:focus,:hover){opacity:.7}.md-footer__link:is(:focus,:hover){opacity:.7}[dir=rtl] .md-footer__link svg{transform:scaleX(-1)}@media screen and (max-width:44.9375em){.md-footer__link--prev .md-footer__title{display:none}}[dir=ltr] .md-footer__link--next{margin-left:auto}[dir=rtl] .md-footer__link--next{margin-right:auto}.md-footer__link--next{text-align:right}[dir=rtl] .md-footer__link--next{text-align:left}.md-footer__title{flex-grow:1;font-size:.9rem;line-height:2.4rem;max-width:calc(100% - 2.4rem);padding:0 1rem;position:relative}.md-footer__button{margin:.2rem;padding:.4rem}.md-footer__direction{font-size:.64rem;left:0;margin-top:-1rem;opacity:.7;padding:0 1rem;position:absolute;right:0}.md-footer-meta{background-color:var(--md-footer-bg-color--dark)}.md-footer-meta__inner{display:flex;flex-wrap:wrap;justify-content:space-between;padding:.2rem}html .md-footer-meta.md-typeset a{color:var(--md-footer-fg-color--light)}html .md-footer-meta.md-typeset a:-webkit-any(:focus,:hover){color:var(--md-footer-fg-color)}html .md-footer-meta.md-typeset a:-moz-any(:focus,:hover){color:var(--md-footer-fg-color)}html .md-footer-meta.md-typeset a:is(:focus,:hover){color:var(--md-footer-fg-color)}.md-copyright{color:var(--md-footer-fg-color--lighter);font-size:.64rem;margin:auto .6rem;padding:.4rem 0;width:100%}@media screen and (min-width:45em){.md-copyright{width:auto}}.md-copyright__highlight{color:var(--md-footer-fg-color--light)}.md-social{margin:0 .4rem;padding:.2rem 0 .6rem}@media screen and (min-width:45em){.md-social{padding:.6rem 0}}.md-social__link{display:inline-block;height:1.6rem;text-align:center;width:1.6rem}.md-social__link:before{line-height:1.9}.md-social__link svg{fill:currentcolor;max-height:.8rem;vertical-align:-25%}.md-typeset .md-button{border:.1rem solid;border-radius:.1rem;color:var(--md-primary-fg-color);cursor:pointer;display:inline-block;font-weight:700;padding:.625em 2em;transition:color 125ms,background-color 125ms,border-color 125ms}.md-typeset .md-button--primary{background-color:var(--md-primary-fg-color);border-color:var(--md-primary-fg-color);color:var(--md-primary-bg-color)}.md-typeset .md-button:-webkit-any(:focus,:hover){background-color:var(--md-accent-fg-color);border-color:var(--md-accent-fg-color);color:var(--md-accent-bg-color)}.md-typeset .md-button:-moz-any(:focus,:hover){background-color:var(--md-accent-fg-color);border-color:var(--md-accent-fg-color);color:var(--md-accent-bg-color)}.md-typeset .md-button:is(:focus,:hover){background-color:var(--md-accent-fg-color);border-color:var(--md-accent-fg-color);color:var(--md-accent-bg-color)}[dir=ltr] .md-typeset .md-input{border-top-left-radius:.1rem}[dir=ltr] .md-typeset .md-input,[dir=rtl] .md-typeset .md-input{border-top-right-radius:.1rem}[dir=rtl] .md-typeset .md-input{border-top-left-radius:.1rem}.md-typeset .md-input{border-bottom:.1rem solid var(--md-default-fg-color--lighter);box-shadow:var(--md-shadow-z1);font-size:.8rem;height:1.8rem;padding:0 .6rem;transition:border .25s,box-shadow .25s}.md-typeset .md-input:-webkit-any(:focus,:hover){border-bottom-color:var(--md-accent-fg-color);box-shadow:var(--md-shadow-z2)}.md-typeset .md-input:-moz-any(:focus,:hover){border-bottom-color:var(--md-accent-fg-color);box-shadow:var(--md-shadow-z2)}.md-typeset .md-input:is(:focus,:hover){border-bottom-color:var(--md-accent-fg-color);box-shadow:var(--md-shadow-z2)}.md-typeset .md-input--stretch{width:100%}.md-header{background-color:var(--md-primary-fg-color);box-shadow:0 0 .2rem transparent,0 .2rem .4rem transparent;color:var(--md-primary-bg-color);left:0;position:-webkit-sticky;position:sticky;right:0;top:0;z-index:4}@media print{.md-header{display:none}}.md-header[data-md-state=shadow]{box-shadow:0 0 .2rem rgba(0,0,0,.1),0 .2rem .4rem rgba(0,0,0,.2);transition:transform .25s cubic-bezier(.1,.7,.1,1),box-shadow .25s}.md-header[data-md-state=hidden]{transform:translateY(-100%);transition:transform .25s cubic-bezier(.8,0,.6,1),box-shadow .25s}.md-header__inner{align-items:center;display:flex;padding:0 .2rem}.md-header__button{color:currentcolor;cursor:pointer;margin:.2rem;outline-color:var(--md-accent-fg-color);padding:.4rem;position:relative;transition:opacity .25s;vertical-align:middle;z-index:1}.md-header__button:hover{opacity:.7}.md-header__button:not([hidden]){display:inline-block}.md-header__button:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}.md-header__button.md-logo{margin:.2rem;padding:.4rem}@media screen and (max-width:76.1875em){.md-header__button.md-logo{display:none}}.md-header__button.md-logo :-webkit-any(img,svg){fill:currentcolor;display:block;height:1.2rem;width:auto}.md-header__button.md-logo :-moz-any(img,svg){fill:currentcolor;display:block;height:1.2rem;width:auto}.md-header__button.md-logo :is(img,svg){fill:currentcolor;display:block;height:1.2rem;width:auto}@media screen and (min-width:60em){.md-header__button[for=__search]{display:none}}.no-js .md-header__button[for=__search]{display:none}[dir=rtl] .md-header__button[for=__search] svg{transform:scaleX(-1)}@media screen and (min-width:76.25em){.md-header__button[for=__drawer]{display:none}}.md-header__topic{display:flex;max-width:100%;position:absolute;transition:transform .4s cubic-bezier(.1,.7,.1,1),opacity .15s}.md-header__topic+.md-header__topic{opacity:0;pointer-events:none;transform:translateX(1.25rem);transition:transform .4s cubic-bezier(1,.7,.1,.1),opacity .15s;z-index:-1}[dir=rtl] .md-header__topic+.md-header__topic{transform:translateX(-1.25rem)}.md-header__topic:first-child{font-weight:700}[dir=ltr] .md-header__title{margin-right:.4rem}[dir=rtl] .md-header__title{margin-left:.4rem}[dir=ltr] .md-header__title{margin-left:1rem}[dir=rtl] .md-header__title{margin-right:1rem}.md-header__title{flex-grow:1;font-size:.9rem;height:2.4rem;line-height:2.4rem}.md-header__title[data-md-state=active] .md-header__topic{opacity:0;pointer-events:none;transform:translateX(-1.25rem);transition:transform .4s cubic-bezier(1,.7,.1,.1),opacity .15s;z-index:-1}[dir=rtl] .md-header__title[data-md-state=active] .md-header__topic{transform:translateX(1.25rem)}.md-header__title[data-md-state=active] .md-header__topic+.md-header__topic{opacity:1;pointer-events:auto;transform:translateX(0);transition:transform .4s cubic-bezier(.1,.7,.1,1),opacity .15s;z-index:0}.md-header__title>.md-header__ellipsis{height:100%;position:relative;width:100%}.md-header__option{display:flex;flex-shrink:0;max-width:100%;transition:max-width 0ms .25s,opacity .25s .25s;white-space:nowrap}[data-md-toggle=search]:checked~.md-header .md-header__option{max-width:0;opacity:0;transition:max-width 0ms,opacity 0ms}.md-header__source{display:none}@media screen and (min-width:60em){[dir=ltr] .md-header__source{margin-left:1rem}[dir=rtl] .md-header__source{margin-right:1rem}.md-header__source{display:block;max-width:11.7rem;width:11.7rem}}@media screen and (min-width:76.25em){[dir=ltr] .md-header__source{margin-left:1.4rem}[dir=rtl] .md-header__source{margin-right:1.4rem}}:root{--md-nav-icon--prev:url('data:image/svg+xml;charset=utf-8,');--md-nav-icon--next:url('data:image/svg+xml;charset=utf-8,');--md-toc-icon:url('data:image/svg+xml;charset=utf-8,')}.md-nav{font-size:.7rem;line-height:1.3}.md-nav__title{display:block;font-weight:700;overflow:hidden;padding:0 .6rem;text-overflow:ellipsis}.md-nav__title .md-nav__button{display:none}.md-nav__title .md-nav__button img{height:100%;width:auto}.md-nav__title .md-nav__button.md-logo :-webkit-any(img,svg){fill:currentcolor;display:block;height:2.4rem;max-width:100%;object-fit:contain;width:auto}.md-nav__title .md-nav__button.md-logo :-moz-any(img,svg){fill:currentcolor;display:block;height:2.4rem;max-width:100%;object-fit:contain;width:auto}.md-nav__title .md-nav__button.md-logo :is(img,svg){fill:currentcolor;display:block;height:2.4rem;max-width:100%;object-fit:contain;width:auto}.md-nav__list{list-style:none;margin:0;padding:0}.md-nav__item{padding:0 .6rem}[dir=ltr] .md-nav__item .md-nav__item{padding-right:0}[dir=rtl] .md-nav__item .md-nav__item{padding-left:0}.md-nav__link{align-items:center;cursor:pointer;display:flex;justify-content:space-between;margin-top:.625em;overflow:hidden;scroll-snap-align:start;text-overflow:ellipsis;transition:color 125ms}.md-nav__link[data-md-state=blur]{color:var(--md-default-fg-color--light)}.md-nav__item .md-nav__link--active{color:var(--md-typeset-a-color)}.md-nav__item .md-nav__link--index [href]{width:100%}.md-nav__link:-webkit-any(:focus,:hover){color:var(--md-accent-fg-color)}.md-nav__link:-moz-any(:focus,:hover){color:var(--md-accent-fg-color)}.md-nav__link:is(:focus,:hover){color:var(--md-accent-fg-color)}.md-nav__link.focus-visible{outline-color:var(--md-accent-fg-color);outline-offset:.2rem}.md-nav--primary .md-nav__link[for=__toc]{display:none}.md-nav--primary .md-nav__link[for=__toc] .md-icon:after{background-color:currentcolor;display:block;height:100%;-webkit-mask-image:var(--md-toc-icon);mask-image:var(--md-toc-icon);width:100%}.md-nav--primary .md-nav__link[for=__toc]~.md-nav{display:none}.md-nav__link>*{cursor:pointer;display:flex}.md-nav__icon{flex-shrink:0}.md-nav__source{display:none}@media screen and (max-width:76.1875em){.md-nav--primary,.md-nav--primary .md-nav{background-color:var(--md-default-bg-color);display:flex;flex-direction:column;height:100%;left:0;position:absolute;right:0;top:0;z-index:1}.md-nav--primary :-webkit-any(.md-nav__title,.md-nav__item){font-size:.8rem;line-height:1.5}.md-nav--primary :-moz-any(.md-nav__title,.md-nav__item){font-size:.8rem;line-height:1.5}.md-nav--primary :is(.md-nav__title,.md-nav__item){font-size:.8rem;line-height:1.5}.md-nav--primary .md-nav__title{background-color:var(--md-default-fg-color--lightest);color:var(--md-default-fg-color--light);cursor:pointer;font-weight:400;height:5.6rem;line-height:2.4rem;padding:3rem .8rem .2rem;position:relative;white-space:nowrap}[dir=ltr] .md-nav--primary .md-nav__title .md-nav__icon{left:.4rem}[dir=rtl] .md-nav--primary .md-nav__title .md-nav__icon{right:.4rem}.md-nav--primary .md-nav__title .md-nav__icon{display:block;height:1.2rem;margin:.2rem;position:absolute;top:.4rem;width:1.2rem}.md-nav--primary .md-nav__title .md-nav__icon:after{background-color:currentcolor;content:"";display:block;height:100%;-webkit-mask-image:var(--md-nav-icon--prev);mask-image:var(--md-nav-icon--prev);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:100%}.md-nav--primary .md-nav__title~.md-nav__list{background-color:var(--md-default-bg-color);box-shadow:0 .05rem 0 var(--md-default-fg-color--lightest) inset;overflow-y:auto;-ms-scroll-snap-type:y mandatory;scroll-snap-type:y mandatory;touch-action:pan-y}.md-nav--primary .md-nav__title~.md-nav__list>:first-child{border-top:0}.md-nav--primary .md-nav__title[for=__drawer]{background-color:var(--md-primary-fg-color);color:var(--md-primary-bg-color);font-weight:700}.md-nav--primary .md-nav__title .md-logo{display:block;left:.2rem;margin:.2rem;padding:.4rem;position:absolute;right:.2rem;top:.2rem}.md-nav--primary .md-nav__list{flex:1}.md-nav--primary .md-nav__item{border-top:.05rem solid var(--md-default-fg-color--lightest);padding:0}.md-nav--primary .md-nav__item--active>.md-nav__link{color:var(--md-typeset-a-color)}.md-nav--primary .md-nav__item--active>.md-nav__link:-webkit-any(:focus,:hover){color:var(--md-accent-fg-color)}.md-nav--primary .md-nav__item--active>.md-nav__link:-moz-any(:focus,:hover){color:var(--md-accent-fg-color)}.md-nav--primary .md-nav__item--active>.md-nav__link:is(:focus,:hover){color:var(--md-accent-fg-color)}.md-nav--primary .md-nav__link{margin-top:0;padding:.6rem .8rem}[dir=ltr] .md-nav--primary .md-nav__link .md-nav__icon{margin-right:-.2rem}[dir=rtl] .md-nav--primary .md-nav__link .md-nav__icon{margin-left:-.2rem}.md-nav--primary .md-nav__link .md-nav__icon{font-size:1.2rem;height:1.2rem;width:1.2rem}.md-nav--primary .md-nav__link .md-nav__icon:after{background-color:currentcolor;content:"";display:block;height:100%;-webkit-mask-image:var(--md-nav-icon--next);mask-image:var(--md-nav-icon--next);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:100%}[dir=rtl] .md-nav--primary .md-nav__icon:after{transform:scale(-1)}.md-nav--primary .md-nav--secondary .md-nav{background-color:initial;position:static}[dir=ltr] .md-nav--primary .md-nav--secondary .md-nav .md-nav__link{padding-left:1.4rem}[dir=rtl] .md-nav--primary .md-nav--secondary .md-nav .md-nav__link{padding-right:1.4rem}[dir=ltr] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav__link{padding-left:2rem}[dir=rtl] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav__link{padding-right:2rem}[dir=ltr] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav__link{padding-left:2.6rem}[dir=rtl] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav__link{padding-right:2.6rem}[dir=ltr] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav .md-nav__link{padding-left:3.2rem}[dir=rtl] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav .md-nav__link{padding-right:3.2rem}.md-nav--secondary{background-color:initial}.md-nav__toggle~.md-nav{display:flex;opacity:0;transform:translateX(100%);transition:transform .25s cubic-bezier(.8,0,.6,1),opacity 125ms 50ms}[dir=rtl] .md-nav__toggle~.md-nav{transform:translateX(-100%)}.md-nav__toggle:checked~.md-nav{opacity:1;transform:translateX(0);transition:transform .25s cubic-bezier(.4,0,.2,1),opacity 125ms 125ms}.md-nav__toggle:checked~.md-nav>.md-nav__list{-webkit-backface-visibility:hidden;backface-visibility:hidden}}@media screen and (max-width:59.9375em){.md-nav--primary .md-nav__link[for=__toc]{display:flex}.md-nav--primary .md-nav__link[for=__toc] .md-icon:after{content:""}.md-nav--primary .md-nav__link[for=__toc]+.md-nav__link{display:none}.md-nav--primary .md-nav__link[for=__toc]~.md-nav{display:flex}.md-nav__source{background-color:var(--md-primary-fg-color--dark);color:var(--md-primary-bg-color);display:block;padding:0 .2rem}}@media screen and (min-width:60em) and (max-width:76.1875em){.md-nav--integrated .md-nav__link[for=__toc]{display:flex}.md-nav--integrated .md-nav__link[for=__toc] .md-icon:after{content:""}.md-nav--integrated .md-nav__link[for=__toc]+.md-nav__link{display:none}.md-nav--integrated .md-nav__link[for=__toc]~.md-nav{display:flex}}@media screen and (min-width:60em){.md-nav--secondary .md-nav__title[for=__toc]{scroll-snap-align:start}.md-nav--secondary .md-nav__title .md-nav__icon{display:none}}@media screen and (min-width:76.25em){.md-nav{transition:max-height .25s cubic-bezier(.86,0,.07,1)}.md-nav--primary .md-nav__title[for=__drawer]{scroll-snap-align:start}.md-nav--primary .md-nav__title .md-nav__icon,.md-nav__toggle~.md-nav{display:none}.md-nav__toggle:-webkit-any(:checked,:indeterminate)~.md-nav{display:block}.md-nav__toggle:-moz-any(:checked,:indeterminate)~.md-nav{display:block}.md-nav__toggle:is(:checked,:indeterminate)~.md-nav{display:block}.md-nav__item--nested>.md-nav>.md-nav__title{display:none}.md-nav__item--section{display:block;margin:1.25em 0}.md-nav__item--section:last-child{margin-bottom:0}.md-nav__item--section>.md-nav__link{font-weight:700;pointer-events:none}.md-nav__item--section>.md-nav__link--index [href]{pointer-events:auto}.md-nav__item--section>.md-nav__link .md-nav__icon{display:none}.md-nav__item--section>.md-nav{display:block}.md-nav__item--section>.md-nav>.md-nav__list>.md-nav__item{padding:0}.md-nav__icon{border-radius:100%;float:right;height:.9rem;transition:background-color .25s,transform .25s;width:.9rem}[dir=rtl] .md-nav__icon{float:left;transform:rotate(180deg)}.md-nav__icon:hover{background-color:var(--md-accent-fg-color--transparent)}.md-nav__icon:after{background-color:currentcolor;content:"";display:inline-block;height:100%;-webkit-mask-image:var(--md-nav-icon--next);mask-image:var(--md-nav-icon--next);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;vertical-align:-.1rem;width:100%}.md-nav__item--nested .md-nav__toggle:checked~.md-nav__link .md-nav__icon,.md-nav__item--nested .md-nav__toggle:indeterminate~.md-nav__link .md-nav__icon{transform:rotate(90deg)}.md-nav--lifted>.md-nav__list>.md-nav__item,.md-nav--lifted>.md-nav__list>.md-nav__item--nested,.md-nav--lifted>.md-nav__title{display:none}.md-nav--lifted>.md-nav__list>.md-nav__item--active{display:block;padding:0}.md-nav--lifted>.md-nav__list>.md-nav__item--active>.md-nav__link{font-weight:700;margin-top:0;padding:0 .6rem;pointer-events:none}.md-nav--lifted>.md-nav__list>.md-nav__item--active>.md-nav__link--index [href]{pointer-events:auto}.md-nav--lifted>.md-nav__list>.md-nav__item--active>.md-nav__link .md-nav__icon{display:none}.md-nav--lifted .md-nav[data-md-level="1"]{display:block}[dir=ltr] .md-nav--lifted .md-nav[data-md-level="1"]>.md-nav__list>.md-nav__item{padding-right:.6rem}[dir=rtl] .md-nav--lifted .md-nav[data-md-level="1"]>.md-nav__list>.md-nav__item{padding-left:.6rem}.md-nav--integrated>.md-nav__list>.md-nav__item--active:not(.md-nav__item--nested){padding:0 .6rem}.md-nav--integrated>.md-nav__list>.md-nav__item--active:not(.md-nav__item--nested)>.md-nav__link{padding:0}[dir=ltr] .md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary{border-left:.05rem solid var(--md-primary-fg-color)}[dir=rtl] .md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary{border-right:.05rem solid var(--md-primary-fg-color)}.md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary{display:block;margin-bottom:1.25em}.md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary>.md-nav__title{display:none}}:root{--md-search-result-icon:url('data:image/svg+xml;charset=utf-8,')}.md-search{position:relative}@media screen and (min-width:60em){.md-search{padding:.2rem 0}}.no-js .md-search{display:none}.md-search__overlay{opacity:0;z-index:1}@media screen and (max-width:59.9375em){[dir=ltr] .md-search__overlay{left:-2.2rem}[dir=rtl] .md-search__overlay{right:-2.2rem}.md-search__overlay{background-color:var(--md-default-bg-color);border-radius:1rem;height:2rem;overflow:hidden;pointer-events:none;position:absolute;top:-1rem;transform-origin:center;transition:transform .3s .1s,opacity .2s .2s;width:2rem}[data-md-toggle=search]:checked~.md-header .md-search__overlay{opacity:1;transition:transform .4s,opacity .1s}}@media screen and (min-width:60em){[dir=ltr] .md-search__overlay{left:0}[dir=rtl] .md-search__overlay{right:0}.md-search__overlay{background-color:rgba(0,0,0,.54);cursor:pointer;height:0;position:fixed;top:0;transition:width 0ms .25s,height 0ms .25s,opacity .25s;width:0}[data-md-toggle=search]:checked~.md-header .md-search__overlay{height:200vh;opacity:1;transition:width 0ms,height 0ms,opacity .25s;width:100%}}@media screen and (max-width:29.9375em){[data-md-toggle=search]:checked~.md-header .md-search__overlay{transform:scale(45)}}@media screen and (min-width:30em) and (max-width:44.9375em){[data-md-toggle=search]:checked~.md-header .md-search__overlay{transform:scale(60)}}@media screen and (min-width:45em) and (max-width:59.9375em){[data-md-toggle=search]:checked~.md-header .md-search__overlay{transform:scale(75)}}.md-search__inner{-webkit-backface-visibility:hidden;backface-visibility:hidden}@media screen and (max-width:59.9375em){[dir=ltr] .md-search__inner{left:0}[dir=rtl] .md-search__inner{right:0}.md-search__inner{height:0;opacity:0;overflow:hidden;position:fixed;top:0;transform:translateX(5%);transition:width 0ms .3s,height 0ms .3s,transform .15s cubic-bezier(.4,0,.2,1) .15s,opacity .15s .15s;width:0;z-index:2}[dir=rtl] .md-search__inner{transform:translateX(-5%)}[data-md-toggle=search]:checked~.md-header .md-search__inner{height:100%;opacity:1;transform:translateX(0);transition:width 0ms 0ms,height 0ms 0ms,transform .15s cubic-bezier(.1,.7,.1,1) .15s,opacity .15s .15s;width:100%}}@media screen and (min-width:60em){.md-search__inner{float:right;padding:.1rem 0;position:relative;transition:width .25s cubic-bezier(.1,.7,.1,1);width:11.7rem}[dir=rtl] .md-search__inner{float:left}}@media screen and (min-width:60em) and (max-width:76.1875em){[data-md-toggle=search]:checked~.md-header .md-search__inner{width:23.4rem}}@media screen and (min-width:76.25em){[data-md-toggle=search]:checked~.md-header .md-search__inner{width:34.4rem}}.md-search__form{background-color:var(--md-default-bg-color);box-shadow:0 0 .6rem transparent;height:2.4rem;position:relative;transition:color .25s,background-color .25s;z-index:2}@media screen and (min-width:60em){.md-search__form{background-color:rgba(0,0,0,.26);border-radius:.1rem;height:1.8rem}.md-search__form:hover{background-color:hsla(0,0%,100%,.12)}}[data-md-toggle=search]:checked~.md-header .md-search__form{background-color:var(--md-default-bg-color);border-radius:.1rem .1rem 0 0;box-shadow:0 0 .6rem rgba(0,0,0,.07);color:var(--md-default-fg-color)}[dir=ltr] .md-search__input{padding-left:3.6rem;padding-right:2.2rem}[dir=rtl] .md-search__input{padding-left:2.2rem;padding-right:3.6rem}.md-search__input{background:transparent;font-size:.9rem;height:100%;position:relative;text-overflow:ellipsis;width:100%;z-index:2}.md-search__input::-moz-placeholder{-moz-transition:color .25s;transition:color .25s}.md-search__input::-ms-input-placeholder{-ms-transition:color .25s;transition:color .25s}.md-search__input::placeholder{transition:color .25s}.md-search__input::-moz-placeholder{color:var(--md-default-fg-color--light)}.md-search__input::-ms-input-placeholder{color:var(--md-default-fg-color--light)}.md-search__input::placeholder,.md-search__input~.md-search__icon{color:var(--md-default-fg-color--light)}.md-search__input::-ms-clear{display:none}@media screen and (max-width:59.9375em){.md-search__input{font-size:.9rem;height:2.4rem;width:100%}}@media screen and (min-width:60em){[dir=ltr] .md-search__input{padding-left:2.2rem}[dir=rtl] .md-search__input{padding-right:2.2rem}.md-search__input{color:inherit;font-size:.8rem}.md-search__input::-moz-placeholder{color:var(--md-primary-bg-color--light)}.md-search__input::-ms-input-placeholder{color:var(--md-primary-bg-color--light)}.md-search__input::placeholder{color:var(--md-primary-bg-color--light)}.md-search__input+.md-search__icon{color:var(--md-primary-bg-color)}[data-md-toggle=search]:checked~.md-header .md-search__input{text-overflow:clip}[data-md-toggle=search]:checked~.md-header .md-search__input::-moz-placeholder{color:var(--md-default-fg-color--light)}[data-md-toggle=search]:checked~.md-header .md-search__input::-ms-input-placeholder{color:var(--md-default-fg-color--light)}[data-md-toggle=search]:checked~.md-header .md-search__input+.md-search__icon,[data-md-toggle=search]:checked~.md-header .md-search__input::placeholder{color:var(--md-default-fg-color--light)}}.md-search__icon{cursor:pointer;display:inline-block;height:1.2rem;transition:color .25s,opacity .25s;width:1.2rem}.md-search__icon:hover{opacity:.7}[dir=ltr] .md-search__icon[for=__search]{left:.5rem}[dir=rtl] .md-search__icon[for=__search]{right:.5rem}.md-search__icon[for=__search]{position:absolute;top:.3rem;z-index:2}[dir=rtl] .md-search__icon[for=__search] svg{transform:scaleX(-1)}@media screen and (max-width:59.9375em){[dir=ltr] .md-search__icon[for=__search]{left:.8rem}[dir=rtl] .md-search__icon[for=__search]{right:.8rem}.md-search__icon[for=__search]{top:.6rem}.md-search__icon[for=__search] svg:first-child{display:none}}@media screen and (min-width:60em){.md-search__icon[for=__search]{pointer-events:none}.md-search__icon[for=__search] svg:last-child{display:none}}[dir=ltr] .md-search__options{right:.5rem}[dir=rtl] .md-search__options{left:.5rem}.md-search__options{pointer-events:none;position:absolute;top:.3rem;z-index:2}@media screen and (max-width:59.9375em){[dir=ltr] .md-search__options{right:.8rem}[dir=rtl] .md-search__options{left:.8rem}.md-search__options{top:.6rem}}[dir=ltr] .md-search__options>*{margin-left:.2rem}[dir=rtl] .md-search__options>*{margin-right:.2rem}.md-search__options>*{color:var(--md-default-fg-color--light);opacity:0;transform:scale(.75);transition:transform .15s cubic-bezier(.1,.7,.1,1),opacity .15s}.md-search__options>:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}[data-md-toggle=search]:checked~.md-header .md-search__input:valid~.md-search__options>*{opacity:1;pointer-events:auto;transform:scale(1)}[data-md-toggle=search]:checked~.md-header .md-search__input:valid~.md-search__options>:hover{opacity:.7}[dir=ltr] .md-search__suggest{padding-left:3.6rem;padding-right:2.2rem}[dir=rtl] .md-search__suggest{padding-left:2.2rem;padding-right:3.6rem}.md-search__suggest{align-items:center;color:var(--md-default-fg-color--lighter);display:flex;font-size:.9rem;height:100%;opacity:0;position:absolute;top:0;transition:opacity 50ms;white-space:nowrap;width:100%}@media screen and (min-width:60em){[dir=ltr] .md-search__suggest{padding-left:2.2rem}[dir=rtl] .md-search__suggest{padding-right:2.2rem}.md-search__suggest{font-size:.8rem}}[data-md-toggle=search]:checked~.md-header .md-search__suggest{opacity:1;transition:opacity .3s .1s}[dir=ltr] .md-search__output{border-bottom-left-radius:.1rem}[dir=ltr] .md-search__output,[dir=rtl] .md-search__output{border-bottom-right-radius:.1rem}[dir=rtl] .md-search__output{border-bottom-left-radius:.1rem}.md-search__output{overflow:hidden;position:absolute;width:100%;z-index:1}@media screen and (max-width:59.9375em){.md-search__output{bottom:0;top:2.4rem}}@media screen and (min-width:60em){.md-search__output{opacity:0;top:1.9rem;transition:opacity .4s}[data-md-toggle=search]:checked~.md-header .md-search__output{box-shadow:var(--md-shadow-z3);opacity:1}}.md-search__scrollwrap{-webkit-backface-visibility:hidden;backface-visibility:hidden;background-color:var(--md-default-bg-color);height:100%;overflow-y:auto;touch-action:pan-y}@media (-webkit-max-device-pixel-ratio:1),(max-resolution:1dppx){.md-search__scrollwrap{transform:translateZ(0)}}@media screen and (min-width:60em) and (max-width:76.1875em){.md-search__scrollwrap{width:23.4rem}}@media screen and (min-width:76.25em){.md-search__scrollwrap{width:34.4rem}}@media screen and (min-width:60em){.md-search__scrollwrap{max-height:0;scrollbar-color:var(--md-default-fg-color--lighter) transparent;scrollbar-width:thin}[data-md-toggle=search]:checked~.md-header .md-search__scrollwrap{max-height:75vh}.md-search__scrollwrap:hover{scrollbar-color:var(--md-accent-fg-color) transparent}.md-search__scrollwrap::-webkit-scrollbar{height:.2rem;width:.2rem}.md-search__scrollwrap::-webkit-scrollbar-thumb{background-color:var(--md-default-fg-color--lighter)}.md-search__scrollwrap::-webkit-scrollbar-thumb:hover{background-color:var(--md-accent-fg-color)}}.md-search-result{color:var(--md-default-fg-color);word-break:break-word}.md-search-result__meta{background-color:var(--md-default-fg-color--lightest);color:var(--md-default-fg-color--light);font-size:.64rem;line-height:1.8rem;padding:0 .8rem;scroll-snap-align:start}@media screen and (min-width:60em){[dir=ltr] .md-search-result__meta{padding-left:2.2rem}[dir=rtl] .md-search-result__meta{padding-right:2.2rem}}.md-search-result__list{list-style:none;margin:0;padding:0}.md-search-result__item{box-shadow:0 -.05rem var(--md-default-fg-color--lightest)}.md-search-result__item:first-child{box-shadow:none}.md-search-result__link{display:block;outline:none;scroll-snap-align:start;transition:background-color .25s}.md-search-result__link:-webkit-any(:focus,:hover){background-color:var(--md-accent-fg-color--transparent)}.md-search-result__link:-moz-any(:focus,:hover){background-color:var(--md-accent-fg-color--transparent)}.md-search-result__link:is(:focus,:hover){background-color:var(--md-accent-fg-color--transparent)}.md-search-result__link:last-child p:last-child{margin-bottom:.6rem}.md-search-result__more summary{color:var(--md-typeset-a-color);cursor:pointer;display:block;font-size:.64rem;outline:none;padding:.75em .8rem;scroll-snap-align:start;transition:color .25s,background-color .25s}@media screen and (min-width:60em){[dir=ltr] .md-search-result__more summary{padding-left:2.2rem}[dir=rtl] .md-search-result__more summary{padding-right:2.2rem}}.md-search-result__more summary:-webkit-any(:focus,:hover){background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}.md-search-result__more summary:-moz-any(:focus,:hover){background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}.md-search-result__more summary:is(:focus,:hover){background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}.md-search-result__more summary::marker{display:none}.md-search-result__more summary::-webkit-details-marker{display:none}.md-search-result__more summary~*>*{opacity:.65}.md-search-result__article{overflow:hidden;padding:0 .8rem;position:relative}@media screen and (min-width:60em){[dir=ltr] .md-search-result__article{padding-left:2.2rem}[dir=rtl] .md-search-result__article{padding-right:2.2rem}}.md-search-result__article--document .md-search-result__title{font-size:.8rem;font-weight:400;line-height:1.4;margin:.55rem 0}[dir=ltr] .md-search-result__icon{left:0}[dir=rtl] .md-search-result__icon{right:0}.md-search-result__icon{color:var(--md-default-fg-color--light);height:1.2rem;margin:.5rem;position:absolute;width:1.2rem}@media screen and (max-width:59.9375em){.md-search-result__icon{display:none}}.md-search-result__icon:after{background-color:currentcolor;content:"";display:inline-block;height:100%;-webkit-mask-image:var(--md-search-result-icon);mask-image:var(--md-search-result-icon);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:100%}[dir=rtl] .md-search-result__icon:after{transform:scaleX(-1)}.md-search-result__title{font-size:.64rem;font-weight:700;line-height:1.6;margin:.5em 0}.md-search-result__teaser{-webkit-box-orient:vertical;-webkit-line-clamp:2;color:var(--md-default-fg-color--light);display:-webkit-box;font-size:.64rem;line-height:1.6;margin:.5em 0;max-height:2rem;overflow:hidden;text-overflow:ellipsis}@media screen and (max-width:44.9375em){.md-search-result__teaser{-webkit-line-clamp:3;max-height:3rem}}@media screen and (min-width:60em) and (max-width:76.1875em){.md-search-result__teaser{-webkit-line-clamp:3;max-height:3rem}}.md-search-result__teaser mark{background-color:initial;text-decoration:underline}.md-search-result__terms{font-size:.64rem;font-style:italic;margin:.5em 0}.md-search-result mark{background-color:initial;color:var(--md-accent-fg-color)}.md-select{position:relative;z-index:1}.md-select__inner{background-color:var(--md-default-bg-color);border-radius:.1rem;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color);left:50%;margin-top:.2rem;max-height:0;opacity:0;position:absolute;top:calc(100% - .2rem);transform:translate3d(-50%,.3rem,0);transition:transform .25s 375ms,opacity .25s .25s,max-height 0ms .5s}.md-select:-webkit-any(:focus-within,:hover) .md-select__inner{max-height:10rem;opacity:1;transform:translate3d(-50%,0,0);-webkit-transition:transform .25s cubic-bezier(.1,.7,.1,1),opacity .25s,max-height 0ms;transition:transform .25s cubic-bezier(.1,.7,.1,1),opacity .25s,max-height 0ms}.md-select:-moz-any(:focus-within,:hover) .md-select__inner{max-height:10rem;opacity:1;transform:translate3d(-50%,0,0);-moz-transition:transform .25s cubic-bezier(.1,.7,.1,1),opacity .25s,max-height 0ms;transition:transform .25s cubic-bezier(.1,.7,.1,1),opacity .25s,max-height 0ms}.md-select:is(:focus-within,:hover) .md-select__inner{max-height:10rem;opacity:1;transform:translate3d(-50%,0,0);transition:transform .25s cubic-bezier(.1,.7,.1,1),opacity .25s,max-height 0ms}.md-select__inner:after{border-bottom:.2rem solid transparent;border-bottom-color:var(--md-default-bg-color);border-left:.2rem solid transparent;border-right:.2rem solid transparent;border-top:0;content:"";height:0;left:50%;margin-left:-.2rem;margin-top:-.2rem;position:absolute;top:0;width:0}.md-select__list{border-radius:.1rem;font-size:.8rem;list-style-type:none;margin:0;max-height:inherit;overflow:auto;padding:0}.md-select__item{line-height:1.8rem}[dir=ltr] .md-select__link{padding-left:.6rem;padding-right:1.2rem}[dir=rtl] .md-select__link{padding-left:1.2rem;padding-right:.6rem}.md-select__link{cursor:pointer;display:block;outline:none;scroll-snap-align:start;transition:background-color .25s,color .25s;width:100%}.md-select__link:-webkit-any(:focus,:hover){color:var(--md-accent-fg-color)}.md-select__link:-moz-any(:focus,:hover){color:var(--md-accent-fg-color)}.md-select__link:is(:focus,:hover){color:var(--md-accent-fg-color)}.md-select__link:focus{background-color:var(--md-default-fg-color--lightest)}.md-sidebar{align-self:flex-start;flex-shrink:0;padding:1.2rem 0;position:-webkit-sticky;position:sticky;top:2.4rem;width:12.1rem}@media print{.md-sidebar{display:none}}@media screen and (max-width:76.1875em){[dir=ltr] .md-sidebar--primary{left:-12.1rem}[dir=rtl] .md-sidebar--primary{right:-12.1rem}.md-sidebar--primary{background-color:var(--md-default-bg-color);display:block;height:100%;position:fixed;top:0;transform:translateX(0);transition:transform .25s cubic-bezier(.4,0,.2,1),box-shadow .25s;width:12.1rem;z-index:5}[data-md-toggle=drawer]:checked~.md-container .md-sidebar--primary{box-shadow:var(--md-shadow-z3);transform:translateX(12.1rem)}[dir=rtl] [data-md-toggle=drawer]:checked~.md-container .md-sidebar--primary{transform:translateX(-12.1rem)}.md-sidebar--primary .md-sidebar__scrollwrap{bottom:0;left:0;margin:0;overflow:hidden;position:absolute;right:0;-ms-scroll-snap-type:none;scroll-snap-type:none;top:0}}@media screen and (min-width:76.25em){.md-sidebar{height:0}.no-js .md-sidebar{height:auto}}.md-sidebar--secondary{display:none;order:2}@media screen and (min-width:60em){.md-sidebar--secondary{height:0}.no-js .md-sidebar--secondary{height:auto}.md-sidebar--secondary:not([hidden]){display:block}.md-sidebar--secondary .md-sidebar__scrollwrap{touch-action:pan-y}}.md-sidebar__scrollwrap{-webkit-backface-visibility:hidden;backface-visibility:hidden;margin:0 .2rem;overflow-y:auto;scrollbar-color:var(--md-default-fg-color--lighter) transparent;scrollbar-width:thin}.md-sidebar__scrollwrap:hover{scrollbar-color:var(--md-accent-fg-color) transparent}.md-sidebar__scrollwrap::-webkit-scrollbar{height:.2rem;width:.2rem}.md-sidebar__scrollwrap::-webkit-scrollbar-thumb{background-color:var(--md-default-fg-color--lighter)}.md-sidebar__scrollwrap::-webkit-scrollbar-thumb:hover{background-color:var(--md-accent-fg-color)}@media screen and (max-width:76.1875em){.md-overlay{background-color:rgba(0,0,0,.54);height:0;opacity:0;position:fixed;top:0;transition:width 0ms .25s,height 0ms .25s,opacity .25s;width:0;z-index:5}[data-md-toggle=drawer]:checked~.md-overlay{height:100%;opacity:1;transition:width 0ms,height 0ms,opacity .25s;width:100%}}@-webkit-keyframes facts{0%{height:0}to{height:.65rem}}@keyframes facts{0%{height:0}to{height:.65rem}}@-webkit-keyframes fact{0%{opacity:0;transform:translateY(100%)}50%{opacity:0}to{opacity:1;transform:translateY(0)}}@keyframes fact{0%{opacity:0;transform:translateY(100%)}50%{opacity:0}to{opacity:1;transform:translateY(0)}}:root{--md-source-forks-icon:url('data:image/svg+xml;charset=utf-8,');--md-source-repositories-icon:url('data:image/svg+xml;charset=utf-8,');--md-source-stars-icon:url('data:image/svg+xml;charset=utf-8,');--md-source-version-icon:url('data:image/svg+xml;charset=utf-8,')}.md-source{-webkit-backface-visibility:hidden;backface-visibility:hidden;display:block;font-size:.65rem;line-height:1.2;outline-color:var(--md-accent-fg-color);transition:opacity .25s;white-space:nowrap}.md-source:hover{opacity:.7}.md-source__icon{display:inline-block;height:2.4rem;vertical-align:middle;width:2rem}[dir=ltr] .md-source__icon svg{margin-left:.6rem}[dir=rtl] .md-source__icon svg{margin-right:.6rem}.md-source__icon svg{margin-top:.6rem}[dir=ltr] .md-source__icon+.md-source__repository{margin-left:-2rem}[dir=rtl] .md-source__icon+.md-source__repository{margin-right:-2rem}[dir=ltr] .md-source__icon+.md-source__repository{padding-left:2rem}[dir=rtl] .md-source__icon+.md-source__repository{padding-right:2rem}[dir=ltr] .md-source__repository{margin-left:.6rem}[dir=rtl] .md-source__repository{margin-right:.6rem}.md-source__repository{display:inline-block;max-width:calc(100% - 1.2rem);overflow:hidden;text-overflow:ellipsis;vertical-align:middle}.md-source__facts{font-size:.55rem;list-style-type:none;margin:.1rem 0 0;opacity:.75;overflow:hidden;padding:0}[data-md-state=done] .md-source__facts{-webkit-animation:facts .25s ease-in;animation:facts .25s ease-in}.md-source__fact{display:inline-block}[data-md-state=done] .md-source__fact{-webkit-animation:fact .4s ease-out;animation:fact .4s ease-out}[dir=ltr] .md-source__fact:before{margin-right:.1rem}[dir=rtl] .md-source__fact:before{margin-left:.1rem}.md-source__fact:before{background-color:currentcolor;content:"";display:inline-block;height:.6rem;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;vertical-align:text-top;width:.6rem}[dir=ltr] .md-source__fact:nth-child(1n+2):before{margin-left:.4rem}[dir=rtl] .md-source__fact:nth-child(1n+2):before{margin-right:.4rem}.md-source__fact--version:before{-webkit-mask-image:var(--md-source-version-icon);mask-image:var(--md-source-version-icon)}.md-source__fact--stars:before{-webkit-mask-image:var(--md-source-stars-icon);mask-image:var(--md-source-stars-icon)}.md-source__fact--forks:before{-webkit-mask-image:var(--md-source-forks-icon);mask-image:var(--md-source-forks-icon)}.md-source__fact--repositories:before{-webkit-mask-image:var(--md-source-repositories-icon);mask-image:var(--md-source-repositories-icon)}.md-tabs{background-color:var(--md-primary-fg-color);color:var(--md-primary-bg-color);overflow:auto;width:100%}@media print{.md-tabs{display:none}}@media screen and (max-width:76.1875em){.md-tabs{display:none}}.md-tabs[data-md-state=hidden]{pointer-events:none}[dir=ltr] .md-tabs__list{margin-left:.2rem}[dir=rtl] .md-tabs__list{margin-right:.2rem}.md-tabs__list{contain:content;list-style:none;margin:0;padding:0;white-space:nowrap}.md-tabs__item{display:inline-block;height:2.4rem;padding-left:.6rem;padding-right:.6rem}.md-tabs__link{-webkit-backface-visibility:hidden;backface-visibility:hidden;display:block;font-size:.7rem;margin-top:.8rem;opacity:.7;outline-color:var(--md-accent-fg-color);outline-offset:.2rem;transition:transform .4s cubic-bezier(.1,.7,.1,1),opacity .25s}.md-tabs__link--active,.md-tabs__link:-webkit-any(:focus,:hover){color:inherit;opacity:1}.md-tabs__link--active,.md-tabs__link:-moz-any(:focus,:hover){color:inherit;opacity:1}.md-tabs__link--active,.md-tabs__link:is(:focus,:hover){color:inherit;opacity:1}.md-tabs__item:nth-child(2) .md-tabs__link{transition-delay:20ms}.md-tabs__item:nth-child(3) .md-tabs__link{transition-delay:40ms}.md-tabs__item:nth-child(4) .md-tabs__link{transition-delay:60ms}.md-tabs__item:nth-child(5) .md-tabs__link{transition-delay:80ms}.md-tabs__item:nth-child(6) .md-tabs__link{transition-delay:.1s}.md-tabs__item:nth-child(7) .md-tabs__link{transition-delay:.12s}.md-tabs__item:nth-child(8) .md-tabs__link{transition-delay:.14s}.md-tabs__item:nth-child(9) .md-tabs__link{transition-delay:.16s}.md-tabs__item:nth-child(10) .md-tabs__link{transition-delay:.18s}.md-tabs__item:nth-child(11) .md-tabs__link{transition-delay:.2s}.md-tabs__item:nth-child(12) .md-tabs__link{transition-delay:.22s}.md-tabs__item:nth-child(13) .md-tabs__link{transition-delay:.24s}.md-tabs__item:nth-child(14) .md-tabs__link{transition-delay:.26s}.md-tabs__item:nth-child(15) .md-tabs__link{transition-delay:.28s}.md-tabs__item:nth-child(16) .md-tabs__link{transition-delay:.3s}.md-tabs[data-md-state=hidden] .md-tabs__link{opacity:0;transform:translateY(50%);transition:transform 0ms .1s,opacity .1s}.md-tags{margin-bottom:.75em}[dir=ltr] .md-tag{margin-right:.5em}[dir=rtl] .md-tag{margin-left:.5em}.md-tag{background:var(--md-default-fg-color--lightest);border-radius:.4rem;display:inline-block;font-size:.64rem;font-weight:700;line-height:1.6;margin-bottom:.5em;padding:.3125em .9375em}.md-tag[href]{-webkit-tap-highlight-color:transparent;color:inherit;outline:none;transition:color 125ms,background-color 125ms}.md-tag[href]:focus,.md-tag[href]:hover{background-color:var(--md-accent-fg-color);color:var(--md-accent-bg-color)}[id]>.md-tag{vertical-align:text-top}@-webkit-keyframes pulse{0%{box-shadow:0 0 0 0 var(--md-default-fg-color--lightest)}75%{box-shadow:0 0 0 .625em transparent}to{box-shadow:0 0 0 0 transparent}}@keyframes pulse{0%{box-shadow:0 0 0 0 var(--md-default-fg-color--lightest)}75%{box-shadow:0 0 0 .625em transparent}to{box-shadow:0 0 0 0 transparent}}:root{--md-tooltip-width:20rem}.md-tooltip{-webkit-backface-visibility:hidden;backface-visibility:hidden;background-color:var(--md-default-bg-color);border-radius:.1rem;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color);left:clamp(var(--md-tooltip-0,0rem) + .8rem,var(--md-tooltip-x),(100vw + var(--md-tooltip-0,0rem) + .8rem - var(--md-tooltip-width) - 2 * .8rem));max-height:0;max-width:calc(100vw - 1.6rem);opacity:0;position:absolute;top:var(--md-tooltip-y);transform:translateY(-.4rem);transition:transform 0ms .25s,opacity .25s,max-height 0ms .25s,z-index .25s;width:var(--md-tooltip-width);z-index:0}:focus-within>.md-tooltip{max-height:1000%;opacity:1;transform:translateY(0);transition:transform .25s cubic-bezier(.1,.7,.1,1),opacity .25s,max-height .25s,z-index 0ms}.focus-visible>.md-tooltip{outline:var(--md-accent-fg-color) auto}.md-tooltip__inner{font-size:.64rem;padding:.8rem}.md-tooltip__inner.md-typeset>:first-child{margin-top:0}.md-tooltip__inner.md-typeset>:last-child{margin-bottom:0}.md-annotation{outline:none;white-space:normal}[dir=rtl] .md-annotation{direction:rtl}.md-annotation:not([hidden]){display:inline-block;line-height:1.325}.md-annotation:focus-within>*{z-index:2}.md-annotation__inner{font-family:var(--md-text-font-family);top:calc(var(--md-tooltip-y) + 1.2ch)}:not(:focus-within)>.md-annotation__inner{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.md-annotation__index{color:#fff;cursor:pointer;margin:0 1ch;position:relative;transition:z-index .25s;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;z-index:0}.md-annotation__index:after{-webkit-animation:pulse 2s infinite;animation:pulse 2s infinite;background-color:var(--md-default-fg-color--lighter);border-radius:2ch;content:"";height:2.2ch;left:-.126em;margin:0 -.4ch;padding:0 .4ch;position:absolute;transition:color .25s,background-color .25s;width:calc(100% + 1.2ch);width:max(2.2ch,100% + 1.2ch);z-index:-1}@media (prefers-reduced-motion){.md-annotation__index:after{-webkit-animation:none;animation:none}}:-webkit-any(:focus-within,:hover)>.md-annotation__index:after{background-color:var(--md-accent-fg-color)}:-moz-any(:focus-within,:hover)>.md-annotation__index:after{background-color:var(--md-accent-fg-color)}:is(:focus-within,:hover)>.md-annotation__index:after{background-color:var(--md-accent-fg-color)}:focus-within>.md-annotation__index:after{-webkit-animation:none;animation:none;transition:color .25s,background-color .25s}.md-annotation__index [data-md-annotation-id]{display:inline-block;line-height:90%}.md-annotation__index [data-md-annotation-id]:before{content:attr(data-md-annotation-id);display:inline-block;padding-bottom:.1em;transition:transform .4s cubic-bezier(.1,.7,.1,1);vertical-align:.0625em}@media not print{.md-annotation__index [data-md-annotation-id]:before{content:"+"}:focus-within>.md-annotation__index [data-md-annotation-id]:before{transform:rotate(45deg)}}:-webkit-any(:focus-within,:hover)>.md-annotation__index{color:var(--md-accent-bg-color)}:-moz-any(:focus-within,:hover)>.md-annotation__index{color:var(--md-accent-bg-color)}:is(:focus-within,:hover)>.md-annotation__index{color:var(--md-accent-bg-color)}:focus-within>.md-annotation__index{-webkit-animation:none;animation:none;transition:none}[dir=ltr] .md-top{margin-left:50%}[dir=rtl] .md-top{margin-right:50%}.md-top{background-color:var(--md-default-bg-color);border-radius:1.6rem;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color--light);font-size:.7rem;outline:none;padding:.4rem .8rem;position:fixed;top:3.2rem;transform:translate(-50%);transition:color 125ms,background-color 125ms,transform 125ms cubic-bezier(.4,0,.2,1),opacity 125ms;z-index:2}@media print{.md-top{display:none}}[dir=rtl] .md-top{transform:translate(50%)}.md-top[data-md-state=hidden]{opacity:0;pointer-events:none;transform:translate(-50%,.2rem);transition-duration:0ms}[dir=rtl] .md-top[data-md-state=hidden]{transform:translate(50%,.2rem)}.md-top:-webkit-any(:focus,:hover){background-color:var(--md-accent-fg-color);color:var(--md-accent-bg-color)}.md-top:-moz-any(:focus,:hover){background-color:var(--md-accent-fg-color);color:var(--md-accent-bg-color)}.md-top:is(:focus,:hover){background-color:var(--md-accent-fg-color);color:var(--md-accent-bg-color)}.md-top svg{display:inline-block;vertical-align:-.5em}@-webkit-keyframes hoverfix{0%{pointer-events:none}}@keyframes hoverfix{0%{pointer-events:none}}:root{--md-version-icon:url('data:image/svg+xml;charset=utf-8,')}.md-version{flex-shrink:0;font-size:.8rem;height:2.4rem}[dir=ltr] .md-version__current{margin-left:1.4rem;margin-right:.4rem}[dir=rtl] .md-version__current{margin-left:.4rem;margin-right:1.4rem}.md-version__current{color:inherit;cursor:pointer;outline:none;position:relative;top:.05rem}[dir=ltr] .md-version__current:after{margin-left:.4rem}[dir=rtl] .md-version__current:after{margin-right:.4rem}.md-version__current:after{background-color:currentcolor;content:"";display:inline-block;height:.6rem;-webkit-mask-image:var(--md-version-icon);mask-image:var(--md-version-icon);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;width:.4rem}.md-version__list{background-color:var(--md-default-bg-color);border-radius:.1rem;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color);list-style-type:none;margin:.2rem .8rem;max-height:0;opacity:0;overflow:auto;padding:0;position:absolute;-ms-scroll-snap-type:y mandatory;scroll-snap-type:y mandatory;top:.15rem;transition:max-height 0ms .5s,opacity .25s .25s;z-index:3}.md-version:-webkit-any(:focus-within,:hover) .md-version__list{max-height:10rem;opacity:1;-webkit-transition:max-height 0ms,opacity .25s;transition:max-height 0ms,opacity .25s}.md-version:-moz-any(:focus-within,:hover) .md-version__list{max-height:10rem;opacity:1;-moz-transition:max-height 0ms,opacity .25s;transition:max-height 0ms,opacity .25s}.md-version:is(:focus-within,:hover) .md-version__list{max-height:10rem;opacity:1;transition:max-height 0ms,opacity .25s}@media (pointer:coarse){.md-version:hover .md-version__list{-webkit-animation:hoverfix .25s forwards;animation:hoverfix .25s forwards}.md-version:focus-within .md-version__list{-webkit-animation:none;animation:none}}.md-version__item{line-height:1.8rem}[dir=ltr] .md-version__link{padding-left:.6rem;padding-right:1.2rem}[dir=rtl] .md-version__link{padding-left:1.2rem;padding-right:.6rem}.md-version__link{cursor:pointer;display:block;outline:none;scroll-snap-align:start;transition:color .25s,background-color .25s;white-space:nowrap;width:100%}.md-version__link:-webkit-any(:focus,:hover){color:var(--md-accent-fg-color)}.md-version__link:-moz-any(:focus,:hover){color:var(--md-accent-fg-color)}.md-version__link:is(:focus,:hover){color:var(--md-accent-fg-color)}.md-version__link:focus{background-color:var(--md-default-fg-color--lightest)}:root{--md-admonition-icon--note:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--abstract:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--info:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--tip:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--success:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--question:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--warning:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--failure:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--danger:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--bug:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--example:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--quote:url('data:image/svg+xml;charset=utf-8,')}.md-typeset :-webkit-any(.admonition,details){background-color:var(--md-admonition-bg-color);border:0 solid #448aff;border-radius:.1rem;box-shadow:var(--md-shadow-z1);color:var(--md-admonition-fg-color);display:flow-root;font-size:.64rem;margin:1.5625em 0;padding:0 .6rem;page-break-inside:avoid}.md-typeset :-moz-any(.admonition,details){background-color:var(--md-admonition-bg-color);border:0 solid #448aff;border-radius:.1rem;box-shadow:var(--md-shadow-z1);color:var(--md-admonition-fg-color);display:flow-root;font-size:.64rem;margin:1.5625em 0;padding:0 .6rem;page-break-inside:avoid}[dir=ltr] .md-typeset :-webkit-any(.admonition,details){border-left-width:.2rem}[dir=ltr] .md-typeset :-moz-any(.admonition,details){border-left-width:.2rem}[dir=ltr] .md-typeset :is(.admonition,details){border-left-width:.2rem}[dir=rtl] .md-typeset :-webkit-any(.admonition,details){border-right-width:.2rem}[dir=rtl] .md-typeset :-moz-any(.admonition,details){border-right-width:.2rem}[dir=rtl] .md-typeset :is(.admonition,details){border-right-width:.2rem}.md-typeset :is(.admonition,details){background-color:var(--md-admonition-bg-color);border:0 solid #448aff;border-radius:.1rem;box-shadow:var(--md-shadow-z1);color:var(--md-admonition-fg-color);display:flow-root;font-size:.64rem;margin:1.5625em 0;padding:0 .6rem;page-break-inside:avoid}@media print{.md-typeset :-webkit-any(.admonition,details){box-shadow:none}.md-typeset :-moz-any(.admonition,details){box-shadow:none}.md-typeset :is(.admonition,details){box-shadow:none}}.md-typeset :-webkit-any(.admonition,details)>*{box-sizing:border-box}.md-typeset :-moz-any(.admonition,details)>*{box-sizing:border-box}.md-typeset :is(.admonition,details)>*{box-sizing:border-box}.md-typeset :-webkit-any(.admonition,details) :-webkit-any(.admonition,details){margin-bottom:1em;margin-top:1em}.md-typeset :-moz-any(.admonition,details) :-moz-any(.admonition,details){margin-bottom:1em;margin-top:1em}.md-typeset :is(.admonition,details) :is(.admonition,details){margin-bottom:1em;margin-top:1em}.md-typeset :-webkit-any(.admonition,details) .md-typeset__scrollwrap{margin:1em -.6rem}.md-typeset :-moz-any(.admonition,details) .md-typeset__scrollwrap{margin:1em -.6rem}.md-typeset :is(.admonition,details) .md-typeset__scrollwrap{margin:1em -.6rem}.md-typeset :-webkit-any(.admonition,details) .md-typeset__table{padding:0 .6rem}.md-typeset :-moz-any(.admonition,details) .md-typeset__table{padding:0 .6rem}.md-typeset :is(.admonition,details) .md-typeset__table{padding:0 .6rem}.md-typeset :-webkit-any(.admonition,details)>.tabbed-set:only-child{margin-top:0}.md-typeset :-moz-any(.admonition,details)>.tabbed-set:only-child{margin-top:0}.md-typeset :is(.admonition,details)>.tabbed-set:only-child{margin-top:0}html .md-typeset :-webkit-any(.admonition,details)>:last-child{margin-bottom:.6rem}html .md-typeset :-moz-any(.admonition,details)>:last-child{margin-bottom:.6rem}html .md-typeset :is(.admonition,details)>:last-child{margin-bottom:.6rem}.md-typeset :-webkit-any(.admonition-title,summary){background-color:rgba(68,138,255,.1);border:0 solid #448aff;font-weight:700;margin-bottom:0;margin-top:0;padding-bottom:.4rem;padding-top:.4rem;position:relative}.md-typeset :-moz-any(.admonition-title,summary){background-color:rgba(68,138,255,.1);border:0 solid #448aff;font-weight:700;margin-bottom:0;margin-top:0;padding-bottom:.4rem;padding-top:.4rem;position:relative}[dir=ltr] .md-typeset :-webkit-any(.admonition-title,summary){margin-left:-.8rem;margin-right:-.6rem}[dir=ltr] .md-typeset :-moz-any(.admonition-title,summary){margin-left:-.8rem;margin-right:-.6rem}[dir=ltr] .md-typeset :is(.admonition-title,summary){margin-left:-.8rem;margin-right:-.6rem}[dir=rtl] .md-typeset :-webkit-any(.admonition-title,summary){margin-left:-.6rem;margin-right:-.8rem}[dir=rtl] .md-typeset :-moz-any(.admonition-title,summary){margin-left:-.6rem;margin-right:-.8rem}[dir=rtl] .md-typeset :is(.admonition-title,summary){margin-left:-.6rem;margin-right:-.8rem}[dir=ltr] .md-typeset :-webkit-any(.admonition-title,summary){padding-left:2rem;padding-right:.6rem}[dir=ltr] .md-typeset :-moz-any(.admonition-title,summary){padding-left:2rem;padding-right:.6rem}[dir=ltr] .md-typeset :is(.admonition-title,summary){padding-left:2rem;padding-right:.6rem}[dir=rtl] .md-typeset :-webkit-any(.admonition-title,summary){padding-left:.6rem;padding-right:2rem}[dir=rtl] .md-typeset :-moz-any(.admonition-title,summary){padding-left:.6rem;padding-right:2rem}[dir=rtl] .md-typeset :is(.admonition-title,summary){padding-left:.6rem;padding-right:2rem}[dir=ltr] .md-typeset :-webkit-any(.admonition-title,summary){border-left-width:.2rem}[dir=ltr] .md-typeset :-moz-any(.admonition-title,summary){border-left-width:.2rem}[dir=ltr] .md-typeset :is(.admonition-title,summary){border-left-width:.2rem}[dir=rtl] .md-typeset :-webkit-any(.admonition-title,summary){border-right-width:.2rem}[dir=rtl] .md-typeset :-moz-any(.admonition-title,summary){border-right-width:.2rem}[dir=rtl] .md-typeset :is(.admonition-title,summary){border-right-width:.2rem}[dir=ltr] .md-typeset :-webkit-any(.admonition-title,summary){border-top-left-radius:.1rem}[dir=ltr] .md-typeset :-moz-any(.admonition-title,summary){border-top-left-radius:.1rem}[dir=ltr] .md-typeset :is(.admonition-title,summary){border-top-left-radius:.1rem}[dir=rtl] .md-typeset :-webkit-any(.admonition-title,summary){border-top-right-radius:.1rem}[dir=rtl] .md-typeset :-moz-any(.admonition-title,summary){border-top-right-radius:.1rem}[dir=rtl] .md-typeset :is(.admonition-title,summary){border-top-right-radius:.1rem}.md-typeset :is(.admonition-title,summary){background-color:rgba(68,138,255,.1);border:0 solid #448aff;font-weight:700;margin-bottom:0;margin-top:0;padding-bottom:.4rem;padding-top:.4rem;position:relative}html .md-typeset :-webkit-any(.admonition-title,summary):last-child{margin-bottom:0}html .md-typeset :-moz-any(.admonition-title,summary):last-child{margin-bottom:0}html .md-typeset :is(.admonition-title,summary):last-child{margin-bottom:0}.md-typeset :-webkit-any(.admonition-title,summary):before{background-color:#448aff;content:"";height:1rem;-webkit-mask-image:var(--md-admonition-icon--note);mask-image:var(--md-admonition-icon--note);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:.625em;width:1rem}.md-typeset :-moz-any(.admonition-title,summary):before{background-color:#448aff;content:"";height:1rem;mask-image:var(--md-admonition-icon--note);mask-repeat:no-repeat;mask-size:contain;position:absolute;top:.625em;width:1rem}[dir=ltr] .md-typeset :-webkit-any(.admonition-title,summary):before{left:.6rem}[dir=ltr] .md-typeset :-moz-any(.admonition-title,summary):before{left:.6rem}[dir=ltr] .md-typeset :is(.admonition-title,summary):before{left:.6rem}[dir=rtl] .md-typeset :-webkit-any(.admonition-title,summary):before{right:.6rem}[dir=rtl] .md-typeset :-moz-any(.admonition-title,summary):before{right:.6rem}[dir=rtl] .md-typeset :is(.admonition-title,summary):before{right:.6rem}.md-typeset :is(.admonition-title,summary):before{background-color:#448aff;content:"";height:1rem;-webkit-mask-image:var(--md-admonition-icon--note);mask-image:var(--md-admonition-icon--note);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:.625em;width:1rem}.md-typeset :-webkit-any(.admonition,details):-webkit-any(.note){border-color:#448aff}.md-typeset :-moz-any(.admonition,details):-moz-any(.note){border-color:#448aff}.md-typeset :is(.admonition,details):is(.note){border-color:#448aff}.md-typeset :-webkit-any(.note)>:-webkit-any(.admonition-title,summary){background-color:rgba(68,138,255,.1);border-color:#448aff}.md-typeset :-moz-any(.note)>:-moz-any(.admonition-title,summary){background-color:rgba(68,138,255,.1);border-color:#448aff}.md-typeset :is(.note)>:is(.admonition-title,summary){background-color:rgba(68,138,255,.1);border-color:#448aff}.md-typeset :-webkit-any(.note)>:-webkit-any(.admonition-title,summary):before{background-color:#448aff;-webkit-mask-image:var(--md-admonition-icon--note);mask-image:var(--md-admonition-icon--note);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain}.md-typeset :-moz-any(.note)>:-moz-any(.admonition-title,summary):before{background-color:#448aff;mask-image:var(--md-admonition-icon--note);mask-repeat:no-repeat;mask-size:contain}.md-typeset :is(.note)>:is(.admonition-title,summary):before{background-color:#448aff;-webkit-mask-image:var(--md-admonition-icon--note);mask-image:var(--md-admonition-icon--note);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain}.md-typeset :-webkit-any(.admonition,details):-webkit-any(.abstract,.summary,.tldr){border-color:#00b0ff}.md-typeset :-moz-any(.admonition,details):-moz-any(.abstract,.summary,.tldr){border-color:#00b0ff}.md-typeset :is(.admonition,details):is(.abstract,.summary,.tldr){border-color:#00b0ff}.md-typeset :-webkit-any(.abstract,.summary,.tldr)>:-webkit-any(.admonition-title,summary){background-color:rgba(0,176,255,.1);border-color:#00b0ff}.md-typeset :-moz-any(.abstract,.summary,.tldr)>:-moz-any(.admonition-title,summary){background-color:rgba(0,176,255,.1);border-color:#00b0ff}.md-typeset :is(.abstract,.summary,.tldr)>:is(.admonition-title,summary){background-color:rgba(0,176,255,.1);border-color:#00b0ff}.md-typeset :-webkit-any(.abstract,.summary,.tldr)>:-webkit-any(.admonition-title,summary):before{background-color:#00b0ff;-webkit-mask-image:var(--md-admonition-icon--abstract);mask-image:var(--md-admonition-icon--abstract);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain}.md-typeset :-moz-any(.abstract,.summary,.tldr)>:-moz-any(.admonition-title,summary):before{background-color:#00b0ff;mask-image:var(--md-admonition-icon--abstract);mask-repeat:no-repeat;mask-size:contain}.md-typeset :is(.abstract,.summary,.tldr)>:is(.admonition-title,summary):before{background-color:#00b0ff;-webkit-mask-image:var(--md-admonition-icon--abstract);mask-image:var(--md-admonition-icon--abstract);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain}.md-typeset :-webkit-any(.admonition,details):-webkit-any(.info,.todo){border-color:#00b8d4}.md-typeset :-moz-any(.admonition,details):-moz-any(.info,.todo){border-color:#00b8d4}.md-typeset :is(.admonition,details):is(.info,.todo){border-color:#00b8d4}.md-typeset :-webkit-any(.info,.todo)>:-webkit-any(.admonition-title,summary){background-color:rgba(0,184,212,.1);border-color:#00b8d4}.md-typeset :-moz-any(.info,.todo)>:-moz-any(.admonition-title,summary){background-color:rgba(0,184,212,.1);border-color:#00b8d4}.md-typeset :is(.info,.todo)>:is(.admonition-title,summary){background-color:rgba(0,184,212,.1);border-color:#00b8d4}.md-typeset :-webkit-any(.info,.todo)>:-webkit-any(.admonition-title,summary):before{background-color:#00b8d4;-webkit-mask-image:var(--md-admonition-icon--info);mask-image:var(--md-admonition-icon--info);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain}.md-typeset :-moz-any(.info,.todo)>:-moz-any(.admonition-title,summary):before{background-color:#00b8d4;mask-image:var(--md-admonition-icon--info);mask-repeat:no-repeat;mask-size:contain}.md-typeset :is(.info,.todo)>:is(.admonition-title,summary):before{background-color:#00b8d4;-webkit-mask-image:var(--md-admonition-icon--info);mask-image:var(--md-admonition-icon--info);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain}.md-typeset :-webkit-any(.admonition,details):-webkit-any(.tip,.hint,.important){border-color:#00bfa5}.md-typeset :-moz-any(.admonition,details):-moz-any(.tip,.hint,.important){border-color:#00bfa5}.md-typeset :is(.admonition,details):is(.tip,.hint,.important){border-color:#00bfa5}.md-typeset :-webkit-any(.tip,.hint,.important)>:-webkit-any(.admonition-title,summary){background-color:rgba(0,191,165,.1);border-color:#00bfa5}.md-typeset :-moz-any(.tip,.hint,.important)>:-moz-any(.admonition-title,summary){background-color:rgba(0,191,165,.1);border-color:#00bfa5}.md-typeset :is(.tip,.hint,.important)>:is(.admonition-title,summary){background-color:rgba(0,191,165,.1);border-color:#00bfa5}.md-typeset :-webkit-any(.tip,.hint,.important)>:-webkit-any(.admonition-title,summary):before{background-color:#00bfa5;-webkit-mask-image:var(--md-admonition-icon--tip);mask-image:var(--md-admonition-icon--tip);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain}.md-typeset :-moz-any(.tip,.hint,.important)>:-moz-any(.admonition-title,summary):before{background-color:#00bfa5;mask-image:var(--md-admonition-icon--tip);mask-repeat:no-repeat;mask-size:contain}.md-typeset :is(.tip,.hint,.important)>:is(.admonition-title,summary):before{background-color:#00bfa5;-webkit-mask-image:var(--md-admonition-icon--tip);mask-image:var(--md-admonition-icon--tip);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain}.md-typeset :-webkit-any(.admonition,details):-webkit-any(.success,.check,.done){border-color:#00c853}.md-typeset :-moz-any(.admonition,details):-moz-any(.success,.check,.done){border-color:#00c853}.md-typeset :is(.admonition,details):is(.success,.check,.done){border-color:#00c853}.md-typeset :-webkit-any(.success,.check,.done)>:-webkit-any(.admonition-title,summary){background-color:rgba(0,200,83,.1);border-color:#00c853}.md-typeset :-moz-any(.success,.check,.done)>:-moz-any(.admonition-title,summary){background-color:rgba(0,200,83,.1);border-color:#00c853}.md-typeset :is(.success,.check,.done)>:is(.admonition-title,summary){background-color:rgba(0,200,83,.1);border-color:#00c853}.md-typeset :-webkit-any(.success,.check,.done)>:-webkit-any(.admonition-title,summary):before{background-color:#00c853;-webkit-mask-image:var(--md-admonition-icon--success);mask-image:var(--md-admonition-icon--success);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain}.md-typeset :-moz-any(.success,.check,.done)>:-moz-any(.admonition-title,summary):before{background-color:#00c853;mask-image:var(--md-admonition-icon--success);mask-repeat:no-repeat;mask-size:contain}.md-typeset :is(.success,.check,.done)>:is(.admonition-title,summary):before{background-color:#00c853;-webkit-mask-image:var(--md-admonition-icon--success);mask-image:var(--md-admonition-icon--success);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain}.md-typeset :-webkit-any(.admonition,details):-webkit-any(.question,.help,.faq){border-color:#64dd17}.md-typeset :-moz-any(.admonition,details):-moz-any(.question,.help,.faq){border-color:#64dd17}.md-typeset :is(.admonition,details):is(.question,.help,.faq){border-color:#64dd17}.md-typeset :-webkit-any(.question,.help,.faq)>:-webkit-any(.admonition-title,summary){background-color:rgba(100,221,23,.1);border-color:#64dd17}.md-typeset :-moz-any(.question,.help,.faq)>:-moz-any(.admonition-title,summary){background-color:rgba(100,221,23,.1);border-color:#64dd17}.md-typeset :is(.question,.help,.faq)>:is(.admonition-title,summary){background-color:rgba(100,221,23,.1);border-color:#64dd17}.md-typeset :-webkit-any(.question,.help,.faq)>:-webkit-any(.admonition-title,summary):before{background-color:#64dd17;-webkit-mask-image:var(--md-admonition-icon--question);mask-image:var(--md-admonition-icon--question);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain}.md-typeset :-moz-any(.question,.help,.faq)>:-moz-any(.admonition-title,summary):before{background-color:#64dd17;mask-image:var(--md-admonition-icon--question);mask-repeat:no-repeat;mask-size:contain}.md-typeset :is(.question,.help,.faq)>:is(.admonition-title,summary):before{background-color:#64dd17;-webkit-mask-image:var(--md-admonition-icon--question);mask-image:var(--md-admonition-icon--question);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain}.md-typeset :-webkit-any(.admonition,details):-webkit-any(.warning,.caution,.attention){border-color:#ff9100}.md-typeset :-moz-any(.admonition,details):-moz-any(.warning,.caution,.attention){border-color:#ff9100}.md-typeset :is(.admonition,details):is(.warning,.caution,.attention){border-color:#ff9100}.md-typeset :-webkit-any(.warning,.caution,.attention)>:-webkit-any(.admonition-title,summary){background-color:rgba(255,145,0,.1);border-color:#ff9100}.md-typeset :-moz-any(.warning,.caution,.attention)>:-moz-any(.admonition-title,summary){background-color:rgba(255,145,0,.1);border-color:#ff9100}.md-typeset :is(.warning,.caution,.attention)>:is(.admonition-title,summary){background-color:rgba(255,145,0,.1);border-color:#ff9100}.md-typeset :-webkit-any(.warning,.caution,.attention)>:-webkit-any(.admonition-title,summary):before{background-color:#ff9100;-webkit-mask-image:var(--md-admonition-icon--warning);mask-image:var(--md-admonition-icon--warning);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain}.md-typeset :-moz-any(.warning,.caution,.attention)>:-moz-any(.admonition-title,summary):before{background-color:#ff9100;mask-image:var(--md-admonition-icon--warning);mask-repeat:no-repeat;mask-size:contain}.md-typeset :is(.warning,.caution,.attention)>:is(.admonition-title,summary):before{background-color:#ff9100;-webkit-mask-image:var(--md-admonition-icon--warning);mask-image:var(--md-admonition-icon--warning);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain}.md-typeset :-webkit-any(.admonition,details):-webkit-any(.failure,.fail,.missing){border-color:#ff5252}.md-typeset :-moz-any(.admonition,details):-moz-any(.failure,.fail,.missing){border-color:#ff5252}.md-typeset :is(.admonition,details):is(.failure,.fail,.missing){border-color:#ff5252}.md-typeset :-webkit-any(.failure,.fail,.missing)>:-webkit-any(.admonition-title,summary){background-color:rgba(255,82,82,.1);border-color:#ff5252}.md-typeset :-moz-any(.failure,.fail,.missing)>:-moz-any(.admonition-title,summary){background-color:rgba(255,82,82,.1);border-color:#ff5252}.md-typeset :is(.failure,.fail,.missing)>:is(.admonition-title,summary){background-color:rgba(255,82,82,.1);border-color:#ff5252}.md-typeset :-webkit-any(.failure,.fail,.missing)>:-webkit-any(.admonition-title,summary):before{background-color:#ff5252;-webkit-mask-image:var(--md-admonition-icon--failure);mask-image:var(--md-admonition-icon--failure);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain}.md-typeset :-moz-any(.failure,.fail,.missing)>:-moz-any(.admonition-title,summary):before{background-color:#ff5252;mask-image:var(--md-admonition-icon--failure);mask-repeat:no-repeat;mask-size:contain}.md-typeset :is(.failure,.fail,.missing)>:is(.admonition-title,summary):before{background-color:#ff5252;-webkit-mask-image:var(--md-admonition-icon--failure);mask-image:var(--md-admonition-icon--failure);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain}.md-typeset :-webkit-any(.admonition,details):-webkit-any(.danger,.error){border-color:#ff1744}.md-typeset :-moz-any(.admonition,details):-moz-any(.danger,.error){border-color:#ff1744}.md-typeset :is(.admonition,details):is(.danger,.error){border-color:#ff1744}.md-typeset :-webkit-any(.danger,.error)>:-webkit-any(.admonition-title,summary){background-color:rgba(255,23,68,.1);border-color:#ff1744}.md-typeset :-moz-any(.danger,.error)>:-moz-any(.admonition-title,summary){background-color:rgba(255,23,68,.1);border-color:#ff1744}.md-typeset :is(.danger,.error)>:is(.admonition-title,summary){background-color:rgba(255,23,68,.1);border-color:#ff1744}.md-typeset :-webkit-any(.danger,.error)>:-webkit-any(.admonition-title,summary):before{background-color:#ff1744;-webkit-mask-image:var(--md-admonition-icon--danger);mask-image:var(--md-admonition-icon--danger);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain}.md-typeset :-moz-any(.danger,.error)>:-moz-any(.admonition-title,summary):before{background-color:#ff1744;mask-image:var(--md-admonition-icon--danger);mask-repeat:no-repeat;mask-size:contain}.md-typeset :is(.danger,.error)>:is(.admonition-title,summary):before{background-color:#ff1744;-webkit-mask-image:var(--md-admonition-icon--danger);mask-image:var(--md-admonition-icon--danger);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain}.md-typeset :-webkit-any(.admonition,details):-webkit-any(.bug){border-color:#f50057}.md-typeset :-moz-any(.admonition,details):-moz-any(.bug){border-color:#f50057}.md-typeset :is(.admonition,details):is(.bug){border-color:#f50057}.md-typeset :-webkit-any(.bug)>:-webkit-any(.admonition-title,summary){background-color:rgba(245,0,87,.1);border-color:#f50057}.md-typeset :-moz-any(.bug)>:-moz-any(.admonition-title,summary){background-color:rgba(245,0,87,.1);border-color:#f50057}.md-typeset :is(.bug)>:is(.admonition-title,summary){background-color:rgba(245,0,87,.1);border-color:#f50057}.md-typeset :-webkit-any(.bug)>:-webkit-any(.admonition-title,summary):before{background-color:#f50057;-webkit-mask-image:var(--md-admonition-icon--bug);mask-image:var(--md-admonition-icon--bug);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain}.md-typeset :-moz-any(.bug)>:-moz-any(.admonition-title,summary):before{background-color:#f50057;mask-image:var(--md-admonition-icon--bug);mask-repeat:no-repeat;mask-size:contain}.md-typeset :is(.bug)>:is(.admonition-title,summary):before{background-color:#f50057;-webkit-mask-image:var(--md-admonition-icon--bug);mask-image:var(--md-admonition-icon--bug);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain}.md-typeset :-webkit-any(.admonition,details):-webkit-any(.example){border-color:#7c4dff}.md-typeset :-moz-any(.admonition,details):-moz-any(.example){border-color:#7c4dff}.md-typeset :is(.admonition,details):is(.example){border-color:#7c4dff}.md-typeset :-webkit-any(.example)>:-webkit-any(.admonition-title,summary){background-color:rgba(124,77,255,.1);border-color:#7c4dff}.md-typeset :-moz-any(.example)>:-moz-any(.admonition-title,summary){background-color:rgba(124,77,255,.1);border-color:#7c4dff}.md-typeset :is(.example)>:is(.admonition-title,summary){background-color:rgba(124,77,255,.1);border-color:#7c4dff}.md-typeset :-webkit-any(.example)>:-webkit-any(.admonition-title,summary):before{background-color:#7c4dff;-webkit-mask-image:var(--md-admonition-icon--example);mask-image:var(--md-admonition-icon--example);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain}.md-typeset :-moz-any(.example)>:-moz-any(.admonition-title,summary):before{background-color:#7c4dff;mask-image:var(--md-admonition-icon--example);mask-repeat:no-repeat;mask-size:contain}.md-typeset :is(.example)>:is(.admonition-title,summary):before{background-color:#7c4dff;-webkit-mask-image:var(--md-admonition-icon--example);mask-image:var(--md-admonition-icon--example);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain}.md-typeset :-webkit-any(.admonition,details):-webkit-any(.quote,.cite){border-color:#9e9e9e}.md-typeset :-moz-any(.admonition,details):-moz-any(.quote,.cite){border-color:#9e9e9e}.md-typeset :is(.admonition,details):is(.quote,.cite){border-color:#9e9e9e}.md-typeset :-webkit-any(.quote,.cite)>:-webkit-any(.admonition-title,summary){background-color:hsla(0,0%,62%,.1);border-color:#9e9e9e}.md-typeset :-moz-any(.quote,.cite)>:-moz-any(.admonition-title,summary){background-color:hsla(0,0%,62%,.1);border-color:#9e9e9e}.md-typeset :is(.quote,.cite)>:is(.admonition-title,summary){background-color:hsla(0,0%,62%,.1);border-color:#9e9e9e}.md-typeset :-webkit-any(.quote,.cite)>:-webkit-any(.admonition-title,summary):before{background-color:#9e9e9e;-webkit-mask-image:var(--md-admonition-icon--quote);mask-image:var(--md-admonition-icon--quote);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain}.md-typeset :-moz-any(.quote,.cite)>:-moz-any(.admonition-title,summary):before{background-color:#9e9e9e;mask-image:var(--md-admonition-icon--quote);mask-repeat:no-repeat;mask-size:contain}.md-typeset :is(.quote,.cite)>:is(.admonition-title,summary):before{background-color:#9e9e9e;-webkit-mask-image:var(--md-admonition-icon--quote);mask-image:var(--md-admonition-icon--quote);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain}:root{--md-footnotes-icon:url('data:image/svg+xml;charset=utf-8,')}.md-typeset .footnote{color:var(--md-default-fg-color--light);font-size:.64rem}[dir=ltr] .md-typeset .footnote>ol{margin-left:0}[dir=rtl] .md-typeset .footnote>ol{margin-right:0}.md-typeset .footnote>ol>li{transition:color 125ms}.md-typeset .footnote>ol>li:target{color:var(--md-default-fg-color)}.md-typeset .footnote>ol>li:focus-within .footnote-backref{opacity:1;transform:translateX(0);transition:none}.md-typeset .footnote>ol>li:-webkit-any(:hover,:target) .footnote-backref{opacity:1;transform:translateX(0)}.md-typeset .footnote>ol>li:-moz-any(:hover,:target) .footnote-backref{opacity:1;transform:translateX(0)}.md-typeset .footnote>ol>li:is(:hover,:target) .footnote-backref{opacity:1;transform:translateX(0)}.md-typeset .footnote>ol>li>:first-child{margin-top:0}.md-typeset .footnote-ref{font-size:.75em;font-weight:700}html .md-typeset .footnote-ref{outline-offset:.1rem}.md-typeset [id^="fnref:"]:target>.footnote-ref{outline:auto}.md-typeset .footnote-backref{color:var(--md-typeset-a-color);display:inline-block;font-size:0;opacity:0;transform:translateX(.25rem);transition:color .25s,transform .25s .25s,opacity 125ms .25s;vertical-align:text-bottom}@media print{.md-typeset .footnote-backref{color:var(--md-typeset-a-color);opacity:1;transform:translateX(0)}}[dir=rtl] .md-typeset .footnote-backref{transform:translateX(-.25rem)}.md-typeset .footnote-backref:hover{color:var(--md-accent-fg-color)}.md-typeset .footnote-backref:before{background-color:currentcolor;content:"";display:inline-block;height:.8rem;-webkit-mask-image:var(--md-footnotes-icon);mask-image:var(--md-footnotes-icon);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:.8rem}[dir=rtl] .md-typeset .footnote-backref:before svg{transform:scaleX(-1)}[dir=ltr] .md-typeset .headerlink{margin-left:.5rem}[dir=rtl] .md-typeset .headerlink{margin-right:.5rem}.md-typeset .headerlink{color:var(--md-default-fg-color--lighter);display:inline-block;opacity:0;transition:color .25s,opacity 125ms}@media print{.md-typeset .headerlink{display:none}}.md-typeset .headerlink:focus,.md-typeset :-webkit-any(:hover,:target)>.headerlink{opacity:1;-webkit-transition:color .25s,opacity 125ms;transition:color .25s,opacity 125ms}.md-typeset .headerlink:focus,.md-typeset :-moz-any(:hover,:target)>.headerlink{opacity:1;-moz-transition:color .25s,opacity 125ms;transition:color .25s,opacity 125ms}.md-typeset .headerlink:focus,.md-typeset :is(:hover,:target)>.headerlink{opacity:1;transition:color .25s,opacity 125ms}.md-typeset .headerlink:-webkit-any(:focus,:hover),.md-typeset :target>.headerlink{color:var(--md-accent-fg-color)}.md-typeset .headerlink:-moz-any(:focus,:hover),.md-typeset :target>.headerlink{color:var(--md-accent-fg-color)}.md-typeset .headerlink:is(:focus,:hover),.md-typeset :target>.headerlink{color:var(--md-accent-fg-color)}.md-typeset :target{--md-scroll-margin:3.6rem;--md-scroll-offset:0rem;scroll-margin-top:calc(var(--md-scroll-margin) - var(--md-scroll-offset))}@media screen and (min-width:76.25em){.md-header--lifted~.md-container .md-typeset :target{--md-scroll-margin:6rem}}.md-typeset :-webkit-any(h1,h2,h3):target{--md-scroll-offset:0.2rem}.md-typeset :-moz-any(h1,h2,h3):target{--md-scroll-offset:0.2rem}.md-typeset :is(h1,h2,h3):target{--md-scroll-offset:0.2rem}.md-typeset h4:target{--md-scroll-offset:0.15rem}.md-typeset div.arithmatex{overflow:auto}@media screen and (max-width:44.9375em){.md-typeset div.arithmatex{margin:0 -.8rem}}.md-typeset div.arithmatex>*{margin-left:auto!important;margin-right:auto!important;padding:0 .8rem;touch-action:auto;width:-webkit-min-content;width:-moz-min-content;width:min-content}.md-typeset div.arithmatex>* mjx-container{margin:0!important}.md-typeset :-webkit-any(del,ins,.comment).critic{-webkit-box-decoration-break:clone;box-decoration-break:clone}.md-typeset :-moz-any(del,ins,.comment).critic{box-decoration-break:clone}.md-typeset :is(del,ins,.comment).critic{-webkit-box-decoration-break:clone;box-decoration-break:clone}.md-typeset del.critic{background-color:var(--md-typeset-del-color)}.md-typeset ins.critic{background-color:var(--md-typeset-ins-color)}.md-typeset .critic.comment{color:var(--md-code-hl-comment-color)}.md-typeset .critic.comment:before{content:"/* "}.md-typeset .critic.comment:after{content:" */"}.md-typeset .critic.block{box-shadow:none;display:block;margin:1em 0;overflow:auto;padding-left:.8rem;padding-right:.8rem}.md-typeset .critic.block>:first-child{margin-top:.5em}.md-typeset .critic.block>:last-child{margin-bottom:.5em}:root{--md-details-icon:url('data:image/svg+xml;charset=utf-8,')}.md-typeset details{display:flow-root;overflow:visible;padding-top:0}.md-typeset details[open]>summary:after{transform:rotate(90deg)}.md-typeset details:not([open]){box-shadow:none;padding-bottom:0}.md-typeset details:not([open])>summary{border-radius:.1rem}[dir=ltr] .md-typeset summary{padding-right:1.8rem}[dir=rtl] .md-typeset summary{padding-left:1.8rem}[dir=ltr] .md-typeset summary{border-top-left-radius:.1rem}[dir=ltr] .md-typeset summary,[dir=rtl] .md-typeset summary{border-top-right-radius:.1rem}[dir=rtl] .md-typeset summary{border-top-left-radius:.1rem}.md-typeset summary{cursor:pointer;display:block;min-height:1rem}.md-typeset summary.focus-visible{outline-color:var(--md-accent-fg-color);outline-offset:.2rem}.md-typeset summary:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}[dir=ltr] .md-typeset summary:after{right:.4rem}[dir=rtl] .md-typeset summary:after{left:.4rem}.md-typeset summary:after{background-color:currentcolor;content:"";height:1rem;-webkit-mask-image:var(--md-details-icon);mask-image:var(--md-details-icon);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:.625em;transform:rotate(0deg);transition:transform .25s;width:1rem}[dir=rtl] .md-typeset summary:after{transform:rotate(180deg)}.md-typeset summary::marker{display:none}.md-typeset summary::-webkit-details-marker{display:none}.md-typeset :-webkit-any(.emojione,.twemoji,.gemoji){display:inline-flex;height:1.125em;vertical-align:text-top}.md-typeset :-moz-any(.emojione,.twemoji,.gemoji){display:inline-flex;height:1.125em;vertical-align:text-top}.md-typeset :is(.emojione,.twemoji,.gemoji){display:inline-flex;height:1.125em;vertical-align:text-top}.md-typeset :-webkit-any(.emojione,.twemoji,.gemoji) svg{fill:currentcolor;max-height:100%;width:1.125em}.md-typeset :-moz-any(.emojione,.twemoji,.gemoji) svg{fill:currentcolor;max-height:100%;width:1.125em}.md-typeset :is(.emojione,.twemoji,.gemoji) svg{fill:currentcolor;max-height:100%;width:1.125em}.highlight :-webkit-any(.o,.ow){color:var(--md-code-hl-operator-color)}.highlight :-moz-any(.o,.ow){color:var(--md-code-hl-operator-color)}.highlight :is(.o,.ow){color:var(--md-code-hl-operator-color)}.highlight .p{color:var(--md-code-hl-punctuation-color)}.highlight :-webkit-any(.cpf,.l,.s,.sb,.sc,.s2,.si,.s1,.ss){color:var(--md-code-hl-string-color)}.highlight :-moz-any(.cpf,.l,.s,.sb,.sc,.s2,.si,.s1,.ss){color:var(--md-code-hl-string-color)}.highlight :is(.cpf,.l,.s,.sb,.sc,.s2,.si,.s1,.ss){color:var(--md-code-hl-string-color)}.highlight :-webkit-any(.cp,.se,.sh,.sr,.sx){color:var(--md-code-hl-special-color)}.highlight :-moz-any(.cp,.se,.sh,.sr,.sx){color:var(--md-code-hl-special-color)}.highlight :is(.cp,.se,.sh,.sr,.sx){color:var(--md-code-hl-special-color)}.highlight :-webkit-any(.m,.mb,.mf,.mh,.mi,.il,.mo){color:var(--md-code-hl-number-color)}.highlight :-moz-any(.m,.mb,.mf,.mh,.mi,.il,.mo){color:var(--md-code-hl-number-color)}.highlight :is(.m,.mb,.mf,.mh,.mi,.il,.mo){color:var(--md-code-hl-number-color)}.highlight :-webkit-any(.k,.kd,.kn,.kp,.kr,.kt){color:var(--md-code-hl-keyword-color)}.highlight :-moz-any(.k,.kd,.kn,.kp,.kr,.kt){color:var(--md-code-hl-keyword-color)}.highlight :is(.k,.kd,.kn,.kp,.kr,.kt){color:var(--md-code-hl-keyword-color)}.highlight :-webkit-any(.kc,.n){color:var(--md-code-hl-name-color)}.highlight :-moz-any(.kc,.n){color:var(--md-code-hl-name-color)}.highlight :is(.kc,.n){color:var(--md-code-hl-name-color)}.highlight :-webkit-any(.no,.nb,.bp){color:var(--md-code-hl-constant-color)}.highlight :-moz-any(.no,.nb,.bp){color:var(--md-code-hl-constant-color)}.highlight :is(.no,.nb,.bp){color:var(--md-code-hl-constant-color)}.highlight :-webkit-any(.nc,.ne,.nf,.nn){color:var(--md-code-hl-function-color)}.highlight :-moz-any(.nc,.ne,.nf,.nn){color:var(--md-code-hl-function-color)}.highlight :is(.nc,.ne,.nf,.nn){color:var(--md-code-hl-function-color)}.highlight :-webkit-any(.nd,.ni,.nl,.nt){color:var(--md-code-hl-keyword-color)}.highlight :-moz-any(.nd,.ni,.nl,.nt){color:var(--md-code-hl-keyword-color)}.highlight :is(.nd,.ni,.nl,.nt){color:var(--md-code-hl-keyword-color)}.highlight :-webkit-any(.c,.cm,.c1,.ch,.cs,.sd){color:var(--md-code-hl-comment-color)}.highlight :-moz-any(.c,.cm,.c1,.ch,.cs,.sd){color:var(--md-code-hl-comment-color)}.highlight :is(.c,.cm,.c1,.ch,.cs,.sd){color:var(--md-code-hl-comment-color)}.highlight :-webkit-any(.na,.nv,.vc,.vg,.vi){color:var(--md-code-hl-variable-color)}.highlight :-moz-any(.na,.nv,.vc,.vg,.vi){color:var(--md-code-hl-variable-color)}.highlight :is(.na,.nv,.vc,.vg,.vi){color:var(--md-code-hl-variable-color)}.highlight :-webkit-any(.ge,.gr,.gh,.go,.gp,.gs,.gu,.gt){color:var(--md-code-hl-generic-color)}.highlight :-moz-any(.ge,.gr,.gh,.go,.gp,.gs,.gu,.gt){color:var(--md-code-hl-generic-color)}.highlight :is(.ge,.gr,.gh,.go,.gp,.gs,.gu,.gt){color:var(--md-code-hl-generic-color)}.highlight :-webkit-any(.gd,.gi){border-radius:.1rem;margin:0 -.125em;padding:0 .125em}.highlight :-moz-any(.gd,.gi){border-radius:.1rem;margin:0 -.125em;padding:0 .125em}.highlight :is(.gd,.gi){border-radius:.1rem;margin:0 -.125em;padding:0 .125em}.highlight .gd{background-color:var(--md-typeset-del-color)}.highlight .gi{background-color:var(--md-typeset-ins-color)}.highlight .hll{background-color:var(--md-code-hl-color);display:block;margin:0 -1.1764705882em;padding:0 1.1764705882em}.highlight span.filename{background-color:var(--md-code-bg-color);border-bottom:.05rem solid var(--md-default-fg-color--lightest);border-top-left-radius:.1rem;border-top-right-radius:.1rem;display:block;font-size:.85em;font-weight:700;margin-top:1em;padding:.6617647059em 1.1764705882em;position:relative}.highlight span.filename+pre{margin-top:0}.highlight span.filename+pre>code{border-top-left-radius:0;border-top-right-radius:0}.highlight [data-linenos]:before{background-color:var(--md-code-bg-color);box-shadow:-.05rem 0 var(--md-default-fg-color--lightest) inset;color:var(--md-default-fg-color--light);content:attr(data-linenos);float:left;left:-1.1764705882em;margin-left:-1.1764705882em;margin-right:1.1764705882em;padding-left:1.1764705882em;position:-webkit-sticky;position:sticky;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;z-index:3}.highlight code a[id]{position:absolute;visibility:hidden}.highlight code[data-md-copying] .hll{display:contents}.highlight code[data-md-copying] .md-annotation{display:none}.highlighttable{display:flow-root}.highlighttable :-webkit-any(tbody,td){display:block;padding:0}.highlighttable :-moz-any(tbody,td){display:block;padding:0}.highlighttable :is(tbody,td){display:block;padding:0}.highlighttable tr{display:flex}.highlighttable pre{margin:0}.highlighttable th.filename{flex-grow:1;padding:0;text-align:left}.highlighttable .linenos{background-color:var(--md-code-bg-color);border-bottom-left-radius:.1rem;border-top-left-radius:.1rem;font-size:.85em;padding:.7720588235em 0 .7720588235em 1.1764705882em;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.highlighttable .linenodiv{box-shadow:-.05rem 0 var(--md-default-fg-color--lightest) inset;padding-right:.5882352941em}.highlighttable .linenodiv pre{color:var(--md-default-fg-color--light);text-align:right}.highlighttable .code{flex:1;min-width:0}.linenodiv a{color:inherit}.md-typeset .highlighttable{direction:ltr;margin:1em 0}.md-typeset .highlighttable code{border-bottom-left-radius:0;border-top-left-radius:0}.md-typeset :-webkit-any(.highlight,.highlighttable)+.result{border:.05rem solid var(--md-code-bg-color);border-bottom-left-radius:.1rem;border-bottom-right-radius:.1rem;border-top-width:.1rem;margin-top:-1.125em;overflow:visible;padding:0 1em}.md-typeset :-moz-any(.highlight,.highlighttable)+.result{border:.05rem solid var(--md-code-bg-color);border-bottom-left-radius:.1rem;border-bottom-right-radius:.1rem;border-top-width:.1rem;margin-top:-1.125em;overflow:visible;padding:0 1em}.md-typeset :is(.highlight,.highlighttable)+.result{border:.05rem solid var(--md-code-bg-color);border-bottom-left-radius:.1rem;border-bottom-right-radius:.1rem;border-top-width:.1rem;margin-top:-1.125em;overflow:visible;padding:0 1em}.md-typeset :-webkit-any(.highlight,.highlighttable)+.result:after{clear:both;content:"";display:block}.md-typeset :-moz-any(.highlight,.highlighttable)+.result:after{clear:both;content:"";display:block}.md-typeset :is(.highlight,.highlighttable)+.result:after{clear:both;content:"";display:block}@media screen and (max-width:44.9375em){.md-content__inner>.highlight{margin:1em -.8rem}.md-content__inner>.highlight .hll{margin:0 -.8rem;padding:0 .8rem}.md-content__inner>.highlight code{border-radius:0}.md-content__inner>.highlight+.result{border-left-width:0;border-radius:0;border-right-width:0;margin-left:-.8rem;margin-right:-.8rem}.md-content__inner>.highlighttable{border-radius:0;margin:1em -.8rem}.md-content__inner>.highlighttable .hll{margin:0 -.8rem;padding:0 .8rem}}.md-typeset .keys kbd:-webkit-any(:before,:after){-moz-osx-font-smoothing:initial;-webkit-font-smoothing:initial;color:inherit;margin:0;position:relative}.md-typeset .keys kbd:-moz-any(:before,:after){-moz-osx-font-smoothing:initial;-webkit-font-smoothing:initial;color:inherit;margin:0;position:relative}.md-typeset .keys kbd:is(:before,:after){-moz-osx-font-smoothing:initial;-webkit-font-smoothing:initial;color:inherit;margin:0;position:relative}.md-typeset .keys span{color:var(--md-default-fg-color--light);padding:0 .2em}.md-typeset .keys .key-alt:before,.md-typeset .keys .key-left-alt:before,.md-typeset .keys .key-right-alt:before{content:"⎇";padding-right:.4em}.md-typeset .keys .key-command:before,.md-typeset .keys .key-left-command:before,.md-typeset .keys .key-right-command:before{content:"⌘";padding-right:.4em}.md-typeset .keys .key-control:before,.md-typeset .keys .key-left-control:before,.md-typeset .keys .key-right-control:before{content:"⌃";padding-right:.4em}.md-typeset .keys .key-left-meta:before,.md-typeset .keys .key-meta:before,.md-typeset .keys .key-right-meta:before{content:"◆";padding-right:.4em}.md-typeset .keys .key-left-option:before,.md-typeset .keys .key-option:before,.md-typeset .keys .key-right-option:before{content:"⌥";padding-right:.4em}.md-typeset .keys .key-left-shift:before,.md-typeset .keys .key-right-shift:before,.md-typeset .keys .key-shift:before{content:"⇧";padding-right:.4em}.md-typeset .keys .key-left-super:before,.md-typeset .keys .key-right-super:before,.md-typeset .keys .key-super:before{content:"❖";padding-right:.4em}.md-typeset .keys .key-left-windows:before,.md-typeset .keys .key-right-windows:before,.md-typeset .keys .key-windows:before{content:"⊞";padding-right:.4em}.md-typeset .keys .key-arrow-down:before{content:"↓";padding-right:.4em}.md-typeset .keys .key-arrow-left:before{content:"←";padding-right:.4em}.md-typeset .keys .key-arrow-right:before{content:"→";padding-right:.4em}.md-typeset .keys .key-arrow-up:before{content:"↑";padding-right:.4em}.md-typeset .keys .key-backspace:before{content:"⌫";padding-right:.4em}.md-typeset .keys .key-backtab:before{content:"⇤";padding-right:.4em}.md-typeset .keys .key-caps-lock:before{content:"⇪";padding-right:.4em}.md-typeset .keys .key-clear:before{content:"⌧";padding-right:.4em}.md-typeset .keys .key-context-menu:before{content:"☰";padding-right:.4em}.md-typeset .keys .key-delete:before{content:"⌦";padding-right:.4em}.md-typeset .keys .key-eject:before{content:"⏏";padding-right:.4em}.md-typeset .keys .key-end:before{content:"⤓";padding-right:.4em}.md-typeset .keys .key-escape:before{content:"⎋";padding-right:.4em}.md-typeset .keys .key-home:before{content:"⤒";padding-right:.4em}.md-typeset .keys .key-insert:before{content:"⎀";padding-right:.4em}.md-typeset .keys .key-page-down:before{content:"⇟";padding-right:.4em}.md-typeset .keys .key-page-up:before{content:"⇞";padding-right:.4em}.md-typeset .keys .key-print-screen:before{content:"⎙";padding-right:.4em}.md-typeset .keys .key-tab:after{content:"⇥";padding-left:.4em}.md-typeset .keys .key-num-enter:after{content:"⌤";padding-left:.4em}.md-typeset .keys .key-enter:after{content:"⏎";padding-left:.4em}.md-typeset .tabbed-set{border-radius:.1rem;display:flex;flex-flow:column wrap;margin:1em 0;position:relative}.md-typeset .tabbed-set>input{height:0;opacity:0;position:absolute;width:0}.md-typeset .tabbed-set>input:target{--md-scroll-offset:0.625em}.md-typeset .tabbed-labels{-ms-overflow-style:none;box-shadow:0 -.05rem var(--md-default-fg-color--lightest) inset;display:flex;max-width:100%;overflow:auto;-ms-scroll-snap-type:x proximity;scroll-snap-type:x proximity;scrollbar-width:none}@media print{.md-typeset .tabbed-labels{display:contents}}@media screen{.js .md-typeset .tabbed-labels{position:relative}.js .md-typeset .tabbed-labels:before{background:var(--md-accent-fg-color);bottom:0;content:"";display:block;height:2px;left:0;position:absolute;transform:translateX(var(--md-indicator-x));transition:width 225ms,transform .25s;transition-timing-function:cubic-bezier(.4,0,.2,1);width:var(--md-indicator-width)}}.md-typeset .tabbed-labels::-webkit-scrollbar{display:none}.md-typeset .tabbed-labels>label{border-bottom:.1rem solid transparent;border-radius:.1rem .1rem 0 0;color:var(--md-default-fg-color--light);cursor:pointer;flex-shrink:0;font-size:.64rem;font-weight:700;padding:.78125em 1.25em .625em;scroll-snap-align:start;transition:background-color .25s,color .25s;white-space:nowrap;width:auto}@media print{.md-typeset .tabbed-labels>label:first-child{order:1}.md-typeset .tabbed-labels>label:nth-child(2){order:2}.md-typeset .tabbed-labels>label:nth-child(3){order:3}.md-typeset .tabbed-labels>label:nth-child(4){order:4}.md-typeset .tabbed-labels>label:nth-child(5){order:5}.md-typeset .tabbed-labels>label:nth-child(6){order:6}.md-typeset .tabbed-labels>label:nth-child(7){order:7}.md-typeset .tabbed-labels>label:nth-child(8){order:8}.md-typeset .tabbed-labels>label:nth-child(9){order:9}.md-typeset .tabbed-labels>label:nth-child(10){order:10}.md-typeset .tabbed-labels>label:nth-child(11){order:11}.md-typeset .tabbed-labels>label:nth-child(12){order:12}.md-typeset .tabbed-labels>label:nth-child(13){order:13}.md-typeset .tabbed-labels>label:nth-child(14){order:14}.md-typeset .tabbed-labels>label:nth-child(15){order:15}.md-typeset .tabbed-labels>label:nth-child(16){order:16}.md-typeset .tabbed-labels>label:nth-child(17){order:17}.md-typeset .tabbed-labels>label:nth-child(18){order:18}.md-typeset .tabbed-labels>label:nth-child(19){order:19}.md-typeset .tabbed-labels>label:nth-child(20){order:20}}.md-typeset .tabbed-labels>label:hover{color:var(--md-accent-fg-color)}.md-typeset .tabbed-content{width:100%}@media print{.md-typeset .tabbed-content{display:contents}}.md-typeset .tabbed-block{display:none}@media print{.md-typeset .tabbed-block{display:block}.md-typeset .tabbed-block:first-child{order:1}.md-typeset .tabbed-block:nth-child(2){order:2}.md-typeset .tabbed-block:nth-child(3){order:3}.md-typeset .tabbed-block:nth-child(4){order:4}.md-typeset .tabbed-block:nth-child(5){order:5}.md-typeset .tabbed-block:nth-child(6){order:6}.md-typeset .tabbed-block:nth-child(7){order:7}.md-typeset .tabbed-block:nth-child(8){order:8}.md-typeset .tabbed-block:nth-child(9){order:9}.md-typeset .tabbed-block:nth-child(10){order:10}.md-typeset .tabbed-block:nth-child(11){order:11}.md-typeset .tabbed-block:nth-child(12){order:12}.md-typeset .tabbed-block:nth-child(13){order:13}.md-typeset .tabbed-block:nth-child(14){order:14}.md-typeset .tabbed-block:nth-child(15){order:15}.md-typeset .tabbed-block:nth-child(16){order:16}.md-typeset .tabbed-block:nth-child(17){order:17}.md-typeset .tabbed-block:nth-child(18){order:18}.md-typeset .tabbed-block:nth-child(19){order:19}.md-typeset .tabbed-block:nth-child(20){order:20}}.md-typeset .tabbed-block>.highlight:first-child>pre:first-child,.md-typeset .tabbed-block>.highlighttable:first-child,.md-typeset .tabbed-block>pre:first-child{margin:0}[dir=ltr] .md-typeset .tabbed-block>.highlight:first-child>pre:first-child>code,[dir=ltr] .md-typeset .tabbed-block>.highlighttable:first-child>code,[dir=ltr] .md-typeset .tabbed-block>pre:first-child>code{border-top-left-radius:0}[dir=ltr] .md-typeset .tabbed-block>.highlight:first-child>pre:first-child>code,[dir=ltr] .md-typeset .tabbed-block>.highlighttable:first-child>code,[dir=ltr] .md-typeset .tabbed-block>pre:first-child>code,[dir=rtl] .md-typeset .tabbed-block>.highlight:first-child>pre:first-child>code,[dir=rtl] .md-typeset .tabbed-block>.highlighttable:first-child>code,[dir=rtl] .md-typeset .tabbed-block>pre:first-child>code{border-top-right-radius:0}[dir=ltr] .md-typeset .tabbed-block>.highlighttable:first-child .linenos,[dir=rtl] .md-typeset .tabbed-block>.highlight:first-child>pre:first-child>code,[dir=rtl] .md-typeset .tabbed-block>.highlighttable:first-child>code,[dir=rtl] .md-typeset .tabbed-block>pre:first-child>code{border-top-left-radius:0}[dir=ltr] .md-typeset .tabbed-block>.highlighttable:first-child .linenos,[dir=rtl] .md-typeset .tabbed-block>.highlighttable:first-child .linenos{border-top-right-radius:0}[dir=rtl] .md-typeset .tabbed-block>.highlighttable:first-child .linenos{border-top-left-radius:0}.md-typeset .tabbed-block>.tabbed-set{margin:0}@media screen and (max-width:44.9375em){[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels{padding-left:.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels{padding-right:.8rem}.md-content__inner>.tabbed-set .tabbed-labels{margin:0 -.8rem;max-width:100vw;scroll-padding-inline-start:.8rem}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels:after{padding-right:.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels:after{padding-left:.8rem}.md-content__inner>.tabbed-set .tabbed-labels:after{content:""}}@media screen{.md-typeset .tabbed-set>input:first-child:checked~.tabbed-labels>:first-child,.md-typeset .tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),.md-typeset .tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),.md-typeset .tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),.md-typeset .tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),.md-typeset .tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),.md-typeset .tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),.md-typeset .tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),.md-typeset .tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),.md-typeset .tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),.md-typeset .tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),.md-typeset .tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),.md-typeset .tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20),.md-typeset .tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),.md-typeset .tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),.md-typeset .tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),.md-typeset .tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),.md-typeset .tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),.md-typeset .tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),.md-typeset .tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9){color:var(--md-accent-fg-color)}.md-typeset .no-js .tabbed-set>input:first-child:checked~.tabbed-labels>:first-child,.md-typeset .no-js .tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),.md-typeset .no-js .tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),.md-typeset .no-js .tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),.md-typeset .no-js .tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),.md-typeset .no-js .tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),.md-typeset .no-js .tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),.md-typeset .no-js .tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),.md-typeset .no-js .tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),.md-typeset .no-js .tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),.md-typeset .no-js .tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),.md-typeset .no-js .tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),.md-typeset .no-js .tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20),.md-typeset .no-js .tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),.md-typeset .no-js .tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),.md-typeset .no-js .tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),.md-typeset .no-js .tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),.md-typeset .no-js .tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),.md-typeset .no-js .tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),.md-typeset .no-js .tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9),.no-js .md-typeset .tabbed-set>input:first-child:checked~.tabbed-labels>:first-child,.no-js .md-typeset .tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),.no-js .md-typeset .tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),.no-js .md-typeset .tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),.no-js .md-typeset .tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),.no-js .md-typeset .tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),.no-js .md-typeset .tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),.no-js .md-typeset .tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),.no-js .md-typeset .tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),.no-js .md-typeset .tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),.no-js .md-typeset .tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),.no-js .md-typeset .tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),.no-js .md-typeset .tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20),.no-js .md-typeset .tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),.no-js .md-typeset .tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),.no-js .md-typeset .tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),.no-js .md-typeset .tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),.no-js .md-typeset .tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),.no-js .md-typeset .tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),.no-js .md-typeset .tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9){border-color:var(--md-accent-fg-color)}}.md-typeset .tabbed-set>input:first-child.focus-visible~.tabbed-labels>:first-child,.md-typeset .tabbed-set>input:nth-child(10).focus-visible~.tabbed-labels>:nth-child(10),.md-typeset .tabbed-set>input:nth-child(11).focus-visible~.tabbed-labels>:nth-child(11),.md-typeset .tabbed-set>input:nth-child(12).focus-visible~.tabbed-labels>:nth-child(12),.md-typeset .tabbed-set>input:nth-child(13).focus-visible~.tabbed-labels>:nth-child(13),.md-typeset .tabbed-set>input:nth-child(14).focus-visible~.tabbed-labels>:nth-child(14),.md-typeset .tabbed-set>input:nth-child(15).focus-visible~.tabbed-labels>:nth-child(15),.md-typeset .tabbed-set>input:nth-child(16).focus-visible~.tabbed-labels>:nth-child(16),.md-typeset .tabbed-set>input:nth-child(17).focus-visible~.tabbed-labels>:nth-child(17),.md-typeset .tabbed-set>input:nth-child(18).focus-visible~.tabbed-labels>:nth-child(18),.md-typeset .tabbed-set>input:nth-child(19).focus-visible~.tabbed-labels>:nth-child(19),.md-typeset .tabbed-set>input:nth-child(2).focus-visible~.tabbed-labels>:nth-child(2),.md-typeset .tabbed-set>input:nth-child(20).focus-visible~.tabbed-labels>:nth-child(20),.md-typeset .tabbed-set>input:nth-child(3).focus-visible~.tabbed-labels>:nth-child(3),.md-typeset .tabbed-set>input:nth-child(4).focus-visible~.tabbed-labels>:nth-child(4),.md-typeset .tabbed-set>input:nth-child(5).focus-visible~.tabbed-labels>:nth-child(5),.md-typeset .tabbed-set>input:nth-child(6).focus-visible~.tabbed-labels>:nth-child(6),.md-typeset .tabbed-set>input:nth-child(7).focus-visible~.tabbed-labels>:nth-child(7),.md-typeset .tabbed-set>input:nth-child(8).focus-visible~.tabbed-labels>:nth-child(8),.md-typeset .tabbed-set>input:nth-child(9).focus-visible~.tabbed-labels>:nth-child(9){background-color:var(--md-accent-fg-color--transparent)}.md-typeset .tabbed-set>input:first-child:checked~.tabbed-content>:first-child,.md-typeset .tabbed-set>input:nth-child(10):checked~.tabbed-content>:nth-child(10),.md-typeset .tabbed-set>input:nth-child(11):checked~.tabbed-content>:nth-child(11),.md-typeset .tabbed-set>input:nth-child(12):checked~.tabbed-content>:nth-child(12),.md-typeset .tabbed-set>input:nth-child(13):checked~.tabbed-content>:nth-child(13),.md-typeset .tabbed-set>input:nth-child(14):checked~.tabbed-content>:nth-child(14),.md-typeset .tabbed-set>input:nth-child(15):checked~.tabbed-content>:nth-child(15),.md-typeset .tabbed-set>input:nth-child(16):checked~.tabbed-content>:nth-child(16),.md-typeset .tabbed-set>input:nth-child(17):checked~.tabbed-content>:nth-child(17),.md-typeset .tabbed-set>input:nth-child(18):checked~.tabbed-content>:nth-child(18),.md-typeset .tabbed-set>input:nth-child(19):checked~.tabbed-content>:nth-child(19),.md-typeset .tabbed-set>input:nth-child(2):checked~.tabbed-content>:nth-child(2),.md-typeset .tabbed-set>input:nth-child(20):checked~.tabbed-content>:nth-child(20),.md-typeset .tabbed-set>input:nth-child(3):checked~.tabbed-content>:nth-child(3),.md-typeset .tabbed-set>input:nth-child(4):checked~.tabbed-content>:nth-child(4),.md-typeset .tabbed-set>input:nth-child(5):checked~.tabbed-content>:nth-child(5),.md-typeset .tabbed-set>input:nth-child(6):checked~.tabbed-content>:nth-child(6),.md-typeset .tabbed-set>input:nth-child(7):checked~.tabbed-content>:nth-child(7),.md-typeset .tabbed-set>input:nth-child(8):checked~.tabbed-content>:nth-child(8),.md-typeset .tabbed-set>input:nth-child(9):checked~.tabbed-content>:nth-child(9){display:block}:root{--md-tasklist-icon:url('data:image/svg+xml;charset=utf-8,');--md-tasklist-icon--checked:url('data:image/svg+xml;charset=utf-8,')}.md-typeset .task-list-item{list-style-type:none;position:relative}[dir=ltr] .md-typeset .task-list-item [type=checkbox]{left:-2em}[dir=rtl] .md-typeset .task-list-item [type=checkbox]{right:-2em}.md-typeset .task-list-item [type=checkbox]{position:absolute;top:.45em}.md-typeset .task-list-control [type=checkbox]{opacity:0;z-index:-1}[dir=ltr] .md-typeset .task-list-indicator:before{left:-1.5em}[dir=rtl] .md-typeset .task-list-indicator:before{right:-1.5em}.md-typeset .task-list-indicator:before{background-color:var(--md-default-fg-color--lightest);content:"";height:1.25em;-webkit-mask-image:var(--md-tasklist-icon);mask-image:var(--md-tasklist-icon);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:.15em;width:1.25em}.md-typeset [type=checkbox]:checked+.task-list-indicator:before{background-color:#00e676;-webkit-mask-image:var(--md-tasklist-icon--checked);mask-image:var(--md-tasklist-icon--checked)}:root>*{--md-mermaid-font-family:var(--md-text-font-family),sans-serif;--md-mermaid-edge-color:var(--md-code-fg-color);--md-mermaid-node-bg-color:var(--md-accent-fg-color--transparent);--md-mermaid-node-fg-color:var(--md-accent-fg-color);--md-mermaid-label-bg-color:var(--md-default-bg-color);--md-mermaid-label-fg-color:var(--md-code-fg-color)}.mermaid{line-height:normal;margin:1em 0}@media screen and (min-width:45em){[dir=ltr] .md-typeset .inline{margin-right:.8rem}[dir=rtl] .md-typeset .inline{margin-left:.8rem}.md-typeset .inline{float:left;margin-bottom:.8rem;margin-top:0;width:11.7rem}[dir=rtl] .md-typeset .inline{float:right}[dir=ltr] .md-typeset .inline.end{margin-left:.8rem;margin-right:0}[dir=rtl] .md-typeset .inline.end{margin-left:0;margin-right:.8rem}.md-typeset .inline.end{float:right}[dir=rtl] .md-typeset .inline.end{float:left}} \ No newline at end of file diff --git a/v0.13.6/assets/stylesheets/main.644de097.min.css.map b/v0.13.6/assets/stylesheets/main.644de097.min.css.map new file mode 100644 index 0000000000..1a08f82d65 --- /dev/null +++ b/v0.13.6/assets/stylesheets/main.644de097.min.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["src/assets/stylesheets/main/extensions/pymdownx/_keys.scss","../../../src/assets/stylesheets/main.scss","src/assets/stylesheets/main/_resets.scss","src/assets/stylesheets/main/_colors.scss","src/assets/stylesheets/main/_icons.scss","src/assets/stylesheets/main/_typeset.scss","src/assets/stylesheets/utilities/_break.scss","src/assets/stylesheets/main/layout/_banner.scss","src/assets/stylesheets/main/layout/_base.scss","src/assets/stylesheets/main/layout/_clipboard.scss","src/assets/stylesheets/main/layout/_content.scss","src/assets/stylesheets/main/layout/_dialog.scss","src/assets/stylesheets/main/layout/_footer.scss","src/assets/stylesheets/main/layout/_form.scss","src/assets/stylesheets/main/layout/_header.scss","src/assets/stylesheets/main/layout/_nav.scss","src/assets/stylesheets/main/layout/_search.scss","src/assets/stylesheets/main/layout/_select.scss","src/assets/stylesheets/main/layout/_sidebar.scss","src/assets/stylesheets/main/layout/_source.scss","src/assets/stylesheets/main/layout/_tabs.scss","src/assets/stylesheets/main/layout/_tag.scss","src/assets/stylesheets/main/layout/_tooltip.scss","src/assets/stylesheets/main/layout/_top.scss","src/assets/stylesheets/main/layout/_version.scss","src/assets/stylesheets/main/extensions/markdown/_admonition.scss","node_modules/material-design-color/material-color.scss","src/assets/stylesheets/main/extensions/markdown/_footnotes.scss","src/assets/stylesheets/main/extensions/markdown/_toc.scss","src/assets/stylesheets/main/extensions/pymdownx/_arithmatex.scss","src/assets/stylesheets/main/extensions/pymdownx/_critic.scss","src/assets/stylesheets/main/extensions/pymdownx/_details.scss","src/assets/stylesheets/main/extensions/pymdownx/_emoji.scss","src/assets/stylesheets/main/extensions/pymdownx/_highlight.scss","src/assets/stylesheets/main/extensions/pymdownx/_tabbed.scss","src/assets/stylesheets/main/extensions/pymdownx/_tasklist.scss","src/assets/stylesheets/main/integrations/_mermaid.scss","src/assets/stylesheets/main/_modifiers.scss"],"names":[],"mappings":"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UAAA,CAQA,sBAAA,CACA,yBAAA,CAPA,U9B2iGN,C8BhiGM,oCACE,wB9BkiGR,C8B7hGI,4BACE,Y9B+hGN,C8B1hGI,4CACE,Y9B4hGN,C+B9mGE,qDACE,mBAAA,CACA,cAAA,CACA,uB/BinGJ,C+BpnGE,kDACE,mBAAA,CACA,cAAA,CACA,uB/BinGJ,C+BpnGE,4CACE,mBAAA,CACA,cAAA,CACA,uB/BinGJ,C+B9mGI,yDAGE,iBAAA,CADA,eAAA,CADA,a/BknGN,C+BnnGI,sDAGE,iBAAA,CADA,eAAA,CADA,a/BknGN,C+BnnGI,gDAGE,iBAAA,CADA,eAAA,CADA,a/BknGN,CgCxnGE,gCACE,sChC2nGJ,CgC5nGE,6BACE,sChC2nGJ,CgC5nGE,uBACE,sChC2nGJ,CgCxnGE,cACE,yChC0nGJ,CgC9mGE,4DACE,oChCgnGJ,CgCjnGE,yDACE,oChCgnGJ,CgCjnGE,mDACE,oChCgnGJ,CgCxmGE,6CACE,qChC0mGJ,CgC3mGE,0CACE,qChC0mGJ,CgC3mGE,oCACE,qChC0mGJ,CgChmGE,oDACE,oChCkmGJ,CgCnmGE,iDACE,oChCkmGJ,CgCnmGE,2CACE,oChCkmGJ,CgCzlGE,gDACE,qChC2lGJ,CgC5lGE,6CACE,qChC2lGJ,CgC5lGE,uCACE,qChC2lGJ,CgCtlGE,gCACE,kChCwlGJ,CgCzlGE,6BACE,kChCwlGJ,CgCzlGE,uBACE,kChCwlGJ,CgCllGE,qCACE,sChColGJ,CgCrlGE,kCACE,sChColGJ,CgCrlGE,4BACE,sChColGJ,CgC7kGE,yCACE,sChC+kGJ,CgChlGE,sCACE,sChC+kGJ,CgChlGE,gCACE,sChC+kGJ,CgCxkGE,yCACE,qChC0kGJ,CgC3kGE,sCACE,qChC0kGJ,CgC3kGE,gCACE,qChC0kGJ,CgCjkGE,gDACE,qChCmkGJ,CgCpkGE,6CACE,qChCmkGJ,CgCpkGE,uCACE,qChCmkGJ,CgC3jGE,6CACE,sChC6jGJ,CgC9jGE,0CACE,sChC6jGJ,CgC9jGE,oCACE,sChC6jGJ,CgCljGE,yDACE,qChCojGJ,CgCrjGE,sDACE,qChCojGJ,CgCrj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,CANF,kBAAA,CALA,UjC09GN,CiC38GM,aAII,6CACE,OjC08GV,CiC38GQ,8CACE,OjC68GV,CiC98GQ,8CACE,OjCg9GV,CiCj9GQ,8CACE,OjCm9GV,CiCp9GQ,8CACE,OjCs9GV,CiCv9GQ,8CACE,OjCy9GV,CiC19GQ,8CACE,OjC49GV,CiC79GQ,8CACE,OjC+9GV,CiCh+GQ,8CACE,OjCk+GV,CiCn+GQ,+CACE,QjCq+GV,CiCt+GQ,+CACE,QjCw+GV,CiCz+GQ,+CACE,QjC2+GV,CiC5+GQ,+CACE,QjC8+GV,CiC/+GQ,+CACE,QjCi/GV,CiCl/GQ,+CACE,QjCo/GV,CiCr/GQ,+CACE,QjCu/GV,CiCx/GQ,+CACE,QjC0/GV,CiC3/GQ,+CACE,QjC6/GV,CiC9/GQ,+CACE,QjCggHV,CiCjgHQ,+CACE,QjCmgHV,CACF,CiC9/GM,uCACE,+BjCggHR,CiC1/GE,4BACE,UjC4/GJ,CiCz/GI,aAJF,4BAKI,gBjC4/GJ,CACF,CiCx/GE,0BACE,YjC0/GJ,CiCv/GI,aAJF,0BAKI,ajC0/GJ,CiCt/GM,sCACE,OjCw/GR,CiCz/GM,uCACE,OjC2/GR,CiC5/GM,uCACE,OjC8/GR,CiC//GM,uCACE,OjCigHR,CiClgHM,uCACE,OjCogHR,CiCrgHM,uCACE,OjCugHR,CiCxgHM,uCACE,OjC0gHR,CiC3gHM,uCACE,OjC6gHR,CiC9gHM,uCACE,OjCghHR,CiCjhHM,wCACE,QjCmhHR,CiCphHM,wCACE,QjCshHR,CiCvhHM,wCACE,QjCyhHR,CiC1hHM,wCACE,QjC4hHR,CiC7hHM,wCACE,QjC+hHR,CiChiHM,wCACE,QjCkiHR,CiCniHM,wCACE,QjCqiHR,CiCtiHM,wCACE,QjCwiHR,CiCziHM,wCACE,QjC2iHR,CiC5iHM,wCACE,QjC8iHR,CiC/iHM,wCACE,QjCijHR,CACF,CiC3iHI,iKAGE,QjC6iHN,CiC1iHM,8MACE,wBjC+iHR,CiChjHM,4ZAEE,yBjC8iHR,CiCziHI,uRACE,wBjC4iHN,CiC7iHI,kJAEE,yBjC2iHN,CiC7iHI,yEAEE,wBjC2iHN,CiCviHI,sCACE,QjCyiHN,CKriHI,wC4BSF,wDAGE,kBjCiiHF,CiCpiHA,wDAGE,mBjCiiHF,CiCpiHA,8CAEE,eAAA,CADA,eAAA,CAGA,iCjCgiHF,CiC5hHE,8DACE,mBjC+hHJ,CiChiHE,8DACE,kBjC+hHJ,CiChiHE,oDAEE,UjC8hHJ,CACF,CiClhHE,cAHF,olDAII,+BjCqhHF,CiClhHE,g8GACE,sCjCohHJ,CACF,CiC/gHA,4sDACE,uDjCkhHF,CiC9gHA,wmDACE,ajCihHF,CkC3vHA,MACE,mVAAA,CAEA,4VlC+vHF,CkCrvHE,4BAEE,oBAAA,CADA,iBlCyvHJ,CkCpvHI,sDAGE,SlCsvHN,CkCzvHI,sDAGE,UlCsvHN,CkCzvHI,4CACE,iBAAA,CACA,SlCuvHN,CkCjvHE,+CAEE,SAAA,CADA,UlCovHJ,CkC/uHE,kDAGE,WlCwvHJ,CkC3vHE,kDAGE,YlCwvHJ,CkC3vHE,wCAME,qDAAA,CAIA,UAAA,CALA,aAAA,CAEA,0CAAA,CAAA,kCAAA,CACA,6BAAA,CAAA,qBAAA,CACA,yBAAA,CAAA,iBAAA,CARA,iBAAA,CACA,SAAA,CAEA,YlCuvHJ,CkC7uHE,gEACE,wBT0Wa,CSzWb,mDAAA,CAAA,2ClC+uHJ,CmChyHA,QACE,8DAAA,CAGA,+CAAA,CACA,iEAAA,CACA,oDAAA,CACA,sDAAA,CACA,mDnCiyHF,CmC7xHA,SAEE,kBAAA,CADA,YnCiyHF,CKxoHI,mC+BhKA,8BAIE,kBpC6yHJ,CoCjzHE,8BAIE,iBpC6yHJ,CoCjzHE,oBACE,UAAA,CAIA,mBAAA,CAFA,YAAA,CADA,apC+yHJ,CoCzyHI,8BACE,WpC2yHN,CoCvyHI,kCAEE,iBAAA,CAAA,cpCyyHN,CoC3yHI,kCAEE,aAAA,CAAA,kBpCyyHN,CoC3yHI,wBACE,WpC0yHN,CoCtyHM,kCACE,UpCwyHR,CACF","file":"main.css"} \ No newline at end of file diff --git a/v0.13.6/assets/stylesheets/palette.e6a45f82.min.css b/v0.13.6/assets/stylesheets/palette.e6a45f82.min.css new file mode 100644 index 0000000000..9d16769c2a --- /dev/null +++ b/v0.13.6/assets/stylesheets/palette.e6a45f82.min.css @@ -0,0 +1 @@ +[data-md-color-accent=red]{--md-accent-fg-color:#ff1947;--md-accent-fg-color--transparent:rgba(255,25,71,.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-accent=pink]{--md-accent-fg-color:#f50056;--md-accent-fg-color--transparent:rgba(245,0,86,.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-accent=purple]{--md-accent-fg-color:#df41fb;--md-accent-fg-color--transparent:rgba(223,65,251,.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-accent=deep-purple]{--md-accent-fg-color:#7c4dff;--md-accent-fg-color--transparent:rgba(124,77,255,.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-accent=indigo]{--md-accent-fg-color:#526cfe;--md-accent-fg-color--transparent:rgba(82,108,254,.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-accent=blue]{--md-accent-fg-color:#4287ff;--md-accent-fg-color--transparent:rgba(66,135,255,.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-accent=light-blue]{--md-accent-fg-color:#0091eb;--md-accent-fg-color--transparent:rgba(0,145,235,.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-accent=cyan]{--md-accent-fg-color:#00bad6;--md-accent-fg-color--transparent:rgba(0,186,214,.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-accent=teal]{--md-accent-fg-color:#00bda4;--md-accent-fg-color--transparent:rgba(0,189,164,.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-accent=green]{--md-accent-fg-color:#00c753;--md-accent-fg-color--transparent:rgba(0,199,83,.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-accent=light-green]{--md-accent-fg-color:#63de17;--md-accent-fg-color--transparent:rgba(99,222,23,.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-accent=lime]{--md-accent-fg-color:#b0eb00;--md-accent-fg-color--transparent:rgba(176,235,0,.1);--md-accent-bg-color:rgba(0,0,0,.87);--md-accent-bg-color--light:rgba(0,0,0,.54)}[data-md-color-accent=yellow]{--md-accent-fg-color:#ffd500;--md-accent-fg-color--transparent:rgba(255,213,0,.1);--md-accent-bg-color:rgba(0,0,0,.87);--md-accent-bg-color--light:rgba(0,0,0,.54)}[data-md-color-accent=amber]{--md-accent-fg-color:#fa0;--md-accent-fg-color--transparent:rgba(255,170,0,.1);--md-accent-bg-color:rgba(0,0,0,.87);--md-accent-bg-color--light:rgba(0,0,0,.54)}[data-md-color-accent=orange]{--md-accent-fg-color:#ff9100;--md-accent-fg-color--transparent:rgba(255,145,0,.1);--md-accent-bg-color:rgba(0,0,0,.87);--md-accent-bg-color--light:rgba(0,0,0,.54)}[data-md-color-accent=deep-orange]{--md-accent-fg-color:#ff6e42;--md-accent-fg-color--transparent:rgba(255,110,66,.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-primary=red]{--md-primary-fg-color:#ef5552;--md-primary-fg-color--light:#e57171;--md-primary-fg-color--dark:#e53734;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-primary=pink]{--md-primary-fg-color:#e92063;--md-primary-fg-color--light:#ec417a;--md-primary-fg-color--dark:#c3185d;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-primary=purple]{--md-primary-fg-color:#ab47bd;--md-primary-fg-color--light:#bb69c9;--md-primary-fg-color--dark:#8c24a8;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-primary=deep-purple]{--md-primary-fg-color:#7e56c2;--md-primary-fg-color--light:#9574cd;--md-primary-fg-color--dark:#673ab6;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-primary=indigo]{--md-primary-fg-color:#4051b5;--md-primary-fg-color--light:#5d6cc0;--md-primary-fg-color--dark:#303fa1;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-primary=blue]{--md-primary-fg-color:#2094f3;--md-primary-fg-color--light:#42a5f5;--md-primary-fg-color--dark:#1975d2;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-primary=light-blue]{--md-primary-fg-color:#02a6f2;--md-primary-fg-color--light:#28b5f6;--md-primary-fg-color--dark:#0287cf;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-primary=cyan]{--md-primary-fg-color:#00bdd6;--md-primary-fg-color--light:#25c5da;--md-primary-fg-color--dark:#0097a8;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-primary=teal]{--md-primary-fg-color:#009485;--md-primary-fg-color--light:#26a699;--md-primary-fg-color--dark:#007a6c;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-primary=green]{--md-primary-fg-color:#4cae4f;--md-primary-fg-color--light:#68bb6c;--md-primary-fg-color--dark:#398e3d;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-primary=light-green]{--md-primary-fg-color:#8bc34b;--md-primary-fg-color--light:#9ccc66;--md-primary-fg-color--dark:#689f38;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-primary=lime]{--md-primary-fg-color:#cbdc38;--md-primary-fg-color--light:#d3e156;--md-primary-fg-color--dark:#b0b52c;--md-primary-bg-color:rgba(0,0,0,.87);--md-primary-bg-color--light:rgba(0,0,0,.54)}[data-md-color-primary=yellow]{--md-primary-fg-color:#ffec3d;--md-primary-fg-color--light:#ffee57;--md-primary-fg-color--dark:#fbc02d;--md-primary-bg-color:rgba(0,0,0,.87);--md-primary-bg-color--light:rgba(0,0,0,.54)}[data-md-color-primary=amber]{--md-primary-fg-color:#ffc105;--md-primary-fg-color--light:#ffc929;--md-primary-fg-color--dark:#ffa200;--md-primary-bg-color:rgba(0,0,0,.87);--md-primary-bg-color--light:rgba(0,0,0,.54)}[data-md-color-primary=orange]{--md-primary-fg-color:#ffa724;--md-primary-fg-color--light:#ffa724;--md-primary-fg-color--dark:#fa8900;--md-primary-bg-color:rgba(0,0,0,.87);--md-primary-bg-color--light:rgba(0,0,0,.54)}[data-md-color-primary=deep-orange]{--md-primary-fg-color:#ff6e42;--md-primary-fg-color--light:#ff8a66;--md-primary-fg-color--dark:#f4511f;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-primary=brown]{--md-primary-fg-color:#795649;--md-primary-fg-color--light:#8d6e62;--md-primary-fg-color--dark:#5d4037;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-primary=grey]{--md-primary-fg-color:#757575;--md-primary-fg-color--light:#9e9e9e;--md-primary-fg-color--dark:#616161;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-primary=blue-grey]{--md-primary-fg-color:#546d78;--md-primary-fg-color--light:#607c8a;--md-primary-fg-color--dark:#455a63;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,.7)}[data-md-color-primary=white]{--md-primary-fg-color:#fff;--md-primary-fg-color--light:hsla(0,0%,100%,.7);--md-primary-fg-color--dark:rgba(0,0,0,.07);--md-primary-bg-color:rgba(0,0,0,.87);--md-primary-bg-color--light:rgba(0,0,0,.54);--md-typeset-a-color:#4051b5}@media screen and (min-width:60em){[data-md-color-primary=white] .md-search__form{background-color:rgba(0,0,0,.07)}[data-md-color-primary=white] .md-search__form:hover{background-color:rgba(0,0,0,.32)}[data-md-color-primary=white] .md-search__input+.md-search__icon{color:rgba(0,0,0,.87)}}@media screen and (min-width:76.25em){[data-md-color-primary=white] .md-tabs{border-bottom:.05rem solid rgba(0,0,0,.07)}}[data-md-color-primary=black]{--md-primary-fg-color:#000;--md-primary-fg-color--light:rgba(0,0,0,.54);--md-primary-fg-color--dark:#000;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,.7);--md-typeset-a-color:#4051b5}[data-md-color-primary=black] .md-header{background-color:#000}@media screen and (max-width:59.9375em){[data-md-color-primary=black] .md-nav__source{background-color:rgba(0,0,0,.87)}}@media screen and (min-width:60em){[data-md-color-primary=black] .md-search__form{background-color:hsla(0,0%,100%,.12)}[data-md-color-primary=black] .md-search__form:hover{background-color:hsla(0,0%,100%,.3)}}@media screen and (max-width:76.1875em){html [data-md-color-primary=black] .md-nav--primary .md-nav__title[for=__drawer]{background-color:#000}}@media screen and (min-width:76.25em){[data-md-color-primary=black] .md-tabs{background-color:#000}}@media screen{[data-md-color-scheme=slate]{--md-hue:232;--md-default-fg-color:hsla(var(--md-hue),75%,95%,1);--md-default-fg-color--light:hsla(var(--md-hue),75%,90%,0.62);--md-default-fg-color--lighter:hsla(var(--md-hue),75%,90%,0.32);--md-default-fg-color--lightest:hsla(var(--md-hue),75%,90%,0.12);--md-default-bg-color:hsla(var(--md-hue),15%,21%,1);--md-default-bg-color--light:hsla(var(--md-hue),15%,21%,0.54);--md-default-bg-color--lighter:hsla(var(--md-hue),15%,21%,0.26);--md-default-bg-color--lightest:hsla(var(--md-hue),15%,21%,0.07);--md-code-fg-color:hsla(var(--md-hue),18%,86%,1);--md-code-bg-color:hsla(var(--md-hue),15%,15%,1);--md-code-hl-color:rgba(66,135,255,.15);--md-code-hl-number-color:#e6695b;--md-code-hl-special-color:#f06090;--md-code-hl-function-color:#c973d9;--md-code-hl-constant-color:#9383e2;--md-code-hl-keyword-color:#6791e0;--md-code-hl-string-color:#2fb170;--md-code-hl-name-color:var(--md-code-fg-color);--md-code-hl-operator-color:var(--md-default-fg-color--light);--md-code-hl-punctuation-color:var(--md-default-fg-color--light);--md-code-hl-comment-color:var(--md-default-fg-color--light);--md-code-hl-generic-color:var(--md-default-fg-color--light);--md-code-hl-variable-color:var(--md-default-fg-color--light);--md-typeset-color:var(--md-default-fg-color);--md-typeset-a-color:var(--md-primary-fg-color);--md-typeset-mark-color:rgba(66,135,255,.3);--md-typeset-kbd-color:hsla(var(--md-hue),15%,94%,0.12);--md-typeset-kbd-accent-color:hsla(var(--md-hue),15%,94%,0.2);--md-typeset-kbd-border-color:hsla(var(--md-hue),15%,14%,1);--md-typeset-table-color:hsla(var(--md-hue),75%,95%,0.12);--md-admonition-bg-color:hsla(var(--md-hue),0%,100%,0.025);--md-footer-bg-color:hsla(var(--md-hue),15%,12%,0.87);--md-footer-bg-color--dark:hsla(var(--md-hue),15%,10%,1)}[data-md-color-scheme=slate][data-md-color-primary=black],[data-md-color-scheme=slate][data-md-color-primary=white]{--md-typeset-a-color:#5d6cc0}[data-md-color-scheme=slate] img[src$="#only-light"]{display:none}[data-md-color-scheme=slate] img[src$="#only-dark"]{display:initial}} \ No newline at end of file diff --git a/v0.13.6/assets/stylesheets/palette.e6a45f82.min.css.map b/v0.13.6/assets/stylesheets/palette.e6a45f82.min.css.map new file mode 100644 index 0000000000..b33c518da1 --- /dev/null +++ b/v0.13.6/assets/stylesheets/palette.e6a45f82.min.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["src/assets/stylesheets/palette/_accent.scss","../../../src/assets/stylesheets/palette.scss","src/assets/stylesheets/palette/_primary.scss","src/assets/stylesheets/utilities/_break.scss","src/assets/stylesheets/palette/_scheme.scss"],"names":[],"mappings":"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","file":"palette.css"} \ No newline at end of file diff --git a/v0.13.6/code-of-conduct/index.html b/v0.13.6/code-of-conduct/index.html new file mode 100644 index 0000000000..11de029c24 --- /dev/null +++ b/v0.13.6/code-of-conduct/index.html @@ -0,0 +1,1933 @@ + + + + + + + + + + + + + + + + + + Code of Conduct - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + +
    +
    +
    + + + +
    +
    +
    + + +
    + +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/contributing/chart/index.html b/v0.13.6/contributing/chart/index.html new file mode 100644 index 0000000000..b58c86ddbe --- /dev/null +++ b/v0.13.6/contributing/chart/index.html @@ -0,0 +1,1981 @@ + + + + + + + + + + + + + + + + + + Helm Chart - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + +
    +
    +
    + + + +
    +
    +
    + + +
    +
    + + + + + + + + +

    Helm Chart

    +

    Chart Changes

    +

    When contributing chart changes please follow the same process as when contributing other content but also please DON’T modify Chart.yaml in the PR as this would result in a chart release when merged and will mean that your PR will need modifying before it can be accepted. The chart version will be updated as part of the PR to release the chart.

    +

    Please DO add your changes to the CHANGELOG.md file in the chart directory under the ## [UNRELEASED] section, if there isn’t an uncommented ## [UNRELEASED] section please copy the commented out template and use that.

    + +
    +
    + + + Last update: + February 3, 2022 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/contributing/crd-source/crd-manifest.yaml b/v0.13.6/contributing/crd-source/crd-manifest.yaml new file mode 100644 index 0000000000..dc1a1a6f1e --- /dev/null +++ b/v0.13.6/contributing/crd-source/crd-manifest.yaml @@ -0,0 +1,94 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.5.0 + api-approved.kubernetes.io: "https://github.com/kubernetes-sigs/external-dns/pull/2007" + creationTimestamp: null + name: dnsendpoints.externaldns.k8s.io +spec: + group: externaldns.k8s.io + names: + kind: DNSEndpoint + listKind: DNSEndpointList + plural: dnsendpoints + singular: dnsendpoint + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: DNSEndpointSpec defines the desired state of DNSEndpoint + properties: + endpoints: + items: + description: Endpoint is a high-level way of a connection between a service and an IP + properties: + dnsName: + description: The hostname of the DNS record + type: string + labels: + additionalProperties: + type: string + description: Labels stores labels defined for the Endpoint + type: object + providerSpecific: + description: ProviderSpecific stores provider specific config + items: + description: ProviderSpecificProperty holds the name and value of a configuration which is specific to individual DNS providers + properties: + name: + type: string + value: + type: string + type: object + type: array + recordTTL: + description: TTL for the record + format: int64 + type: integer + recordType: + description: RecordType type of record, e.g. CNAME, A, SRV, TXT etc + type: string + setIdentifier: + description: Identifier to distinguish multiple records with the same name and type (e.g. Route53 records with routing policies other than 'simple') + type: string + targets: + description: The targets the DNS record points to + items: + type: string + type: array + type: object + type: array + type: object + status: + description: DNSEndpointStatus defines the observed state of DNSEndpoint + properties: + observedGeneration: + description: The generation observed by the external-dns controller. + format: int64 + type: integer + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/v0.13.6/contributing/crd-source/dnsendpoint-aws-example.yaml b/v0.13.6/contributing/crd-source/dnsendpoint-aws-example.yaml new file mode 100644 index 0000000000..e437f0f047 --- /dev/null +++ b/v0.13.6/contributing/crd-source/dnsendpoint-aws-example.yaml @@ -0,0 +1,18 @@ +apiVersion: externaldns.k8s.io/v1alpha1 +kind: DNSEndpoint +metadata: + name: examplednsrecord +spec: + endpoints: + - dnsName: subdomain.foo.bar.com + providerSpecific: + - name: "aws/failover" + value: "PRIMARY" + - name: "aws/health-check-id" + value: "asdf1234-as12-as12-as12-asdf12345678" + - name: "aws/evaluate-target-health" + value: "true" + recordType: CNAME + setIdentifier: some-unique-id + targets: + - other-subdomain.foo.bar.com diff --git a/v0.13.6/contributing/crd-source/dnsendpoint-example.yaml b/v0.13.6/contributing/crd-source/dnsendpoint-example.yaml new file mode 100644 index 0000000000..2ed7d7fa01 --- /dev/null +++ b/v0.13.6/contributing/crd-source/dnsendpoint-example.yaml @@ -0,0 +1,15 @@ +apiVersion: externaldns.k8s.io/v1alpha1 +kind: DNSEndpoint +metadata: + name: examplednsrecord +spec: + endpoints: + - dnsName: foo.bar.com + recordTTL: 180 + recordType: A + targets: + - 192.168.99.216 + # Provider specific configurations are set like an annotation would on other sources + providerSpecific: + - name: external-dns.alpha.kubernetes.io/cloudflare-proxied + value: "true" diff --git a/v0.13.6/contributing/crd-source/index.html b/v0.13.6/contributing/crd-source/index.html new file mode 100644 index 0000000000..1a1ad03d8b --- /dev/null +++ b/v0.13.6/contributing/crd-source/index.html @@ -0,0 +1,2139 @@ + + + + + + + + + + + + + + + + + + CRD Source - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + +
    +
    +
    + + + +
    +
    +
    + + +
    +
    + + + + + + + + +

    CRD Source

    +

    CRD source provides a generic mechanism to manage DNS records in your favourite DNS provider supported by external-dns.

    +

    Details

    +

    CRD source watches for a user specified CRD to extract Endpoints from its Spec.
    +So users need to create such a CRD and register it to the kubernetes cluster and then create new object(s) of the CRD specifying the Endpoints.

    +

    Registering CRD

    +

    Here is typical example of CRD API type which provides Endpoints to CRD source:

    +
    type TTL int64
    +type Targets []string
    +type ProviderSpecificProperty struct {
    +    Name  string `json:"name,omitempty"`
    +    Value string `json:"value,omitempty"`
    +}
    +type ProviderSpecific []ProviderSpecificProperty
    +type Labels map[string]string
    +
    +type Endpoint struct {
    +    // The hostname of the DNS record
    +    DNSName string `json:"dnsName,omitempty"`
    +    // The targets the DNS record points to
    +    Targets Targets `json:"targets,omitempty"`
    +    // RecordType type of record, e.g. CNAME, A, SRV, TXT etc
    +    RecordType string `json:"recordType,omitempty"`
    +    // TTL for the record
    +    RecordTTL TTL `json:"recordTTL,omitempty"`
    +    // Labels stores labels defined for the Endpoint
    +    // +optional
    +    Labels Labels `json:"labels,omitempty"`
    +    // ProviderSpecific stores provider specific config
    +    // +optional
    +    ProviderSpecific ProviderSpecific `json:"providerSpecific,omitempty"`
    +}
    +
    +type DNSEndpointSpec struct {
    +    Endpoints []*Endpoint `json:"endpoints,omitempty"`
    +}
    +
    +type DNSEndpointStatus struct {
    +    // The generation observed by the external-dns controller.
    +    // +optional
    +    ObservedGeneration int64 `json:"observedGeneration,omitempty"`
    +}
    +
    +// +genclient
    +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
    +
    +// DNSEndpoint is the CRD wrapper for Endpoint
    +// +k8s:openapi-gen=true
    +// +kubebuilder:resource:path=dnsendpoints
    +// +kubebuilder:subresource:status
    +type DNSEndpoint struct {
    +    metav1.TypeMeta   `json:",inline"`
    +    metav1.ObjectMeta `json:"metadata,omitempty"`
    +
    +    Spec   DNSEndpointSpec   `json:"spec,omitempty"`
    +    Status DNSEndpointStatus `json:"status,omitempty"`
    +}
    +
    +

    Refer to kubebuilder to create and register the CRD.

    +

    Usage

    +

    One can use CRD source by specifying --source flag with crd and specifying the ApiVersion and Kind of the CRD with --crd-source-apiversion and crd-source-kind respectively.
    +for e.g:

    +
    $ build/external-dns --source crd --crd-source-apiversion externaldns.k8s.io/v1alpha1  --crd-source-kind DNSEndpoint --provider inmemory --once --dry-run
    +
    +

    Creating DNS Records

    +

    Create the objects of CRD type by filling in the fields of CRD and DNS record would be created accordingly.

    +

    Example

    +

    Here is an example CRD manifest generated by kubebuilder.
    +Apply this to register the CRD

    +
    $ kubectl apply --validate=false -f docs/contributing/crd-source/crd-manifest.yaml
    +customresourcedefinition.apiextensions.k8s.io "dnsendpoints.externaldns.k8s.io" created
    +
    +

    Then you can create the dns-endpoint yaml similar to dnsendpoint-example

    +
    $ kubectl apply -f docs/contributing/crd-source/dnsendpoint-example.yaml
    +dnsendpoint.externaldns.k8s.io "examplednsrecord" created
    +
    +

    Run external-dns in dry-mode to see whether external-dns picks up the DNS record from CRD.

    +
    $ build/external-dns --source crd --crd-source-apiversion externaldns.k8s.io/v1alpha1  --crd-source-kind DNSEndpoint --provider inmemory --once --dry-run
    +INFO[0000] running in dry-run mode. No changes to DNS records will be made.
    +INFO[0000] Connected to cluster at https://192.168.99.100:8443
    +INFO[0000] CREATE: foo.bar.com 180 IN A 192.168.99.216
    +INFO[0000] CREATE: foo.bar.com 0 IN TXT "heritage=external-dns,external-dns/owner=default"
    +
    +

    RBAC configuration

    +

    If you use RBAC, extend the external-dns ClusterRole with:
    +

    - apiGroups: ["externaldns.k8s.io"]
    +  resources: ["dnsendpoints"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: ["externaldns.k8s.io"]
    +  resources: ["dnsendpoints/status"]
    +  verbs: ["*"]
    +

    + +
    +
    + + + Last update: + March 6, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/contributing/getting-started/index.html b/v0.13.6/contributing/getting-started/index.html new file mode 100644 index 0000000000..60e24db766 --- /dev/null +++ b/v0.13.6/contributing/getting-started/index.html @@ -0,0 +1,1971 @@ + + + + + + + + + + + + + + + + + + Quick Start - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + +
    +
    +
    + + + +
    +
    +
    + + +
    +
    + + + + + + + + +

    Quick Start

    + +

    Compile and run locally against a remote k8s cluster.
    +

    git clone https://github.com/kubernetes-sigs/external-dns.git && cd external-dns
    +make build
    +# login to remote k8s cluster
    +./build/external-dns --source=service --provider=inmemory --once
    +

    +

    Run linting, unit tests, and coverage report.
    +

    make lint
    +make test
    +make cover-html
    +

    +

    Build container image.
    +

    make build.push IMAGE=your-registry/external-dns
    +

    +

    Design

    +

    ExternalDNS’s sources of DNS records live in package source. They implement the Source interface that has a single method Endpoints which returns the represented source’s objects converted to Endpoints. Endpoints are just a tuple of DNS name and target where target can be an IP or another hostname.

    +

    For example, the ServiceSource returns all Services converted to Endpoints where the hostname is the value of the external-dns.alpha.kubernetes.io/hostname annotation and the target is the IP of the load balancer or where the hostname is the value of the external-dns.alpha.kubernetes.io/internal-hostname annotation and the target is the IP of the service ClusterIP.

    +

    This list of endpoints is passed to the Plan which determines the difference between the current DNS records and the desired list of Endpoints.

    +

    Once the difference has been figured out the list of intended changes is passed to a Registry which live in the registry package. The registry is a wrapper and access point to DNS provider. Registry implements the ownership concept by marking owned records and filtering out records not owned by ExternalDNS before passing them to DNS provider.

    +

    The provider is the adapter to the DNS provider, e.g. Google Cloud DNS. It implements two methods: ApplyChanges to apply a set of changes filtered by Registry and Records to retrieve the current list of records from the DNS provider.

    +

    The orchestration between the different components is controlled by the controller.

    +

    You can pick which Source and Provider to use at runtime via the --source and --provider flags, respectively.

    +

    Adding a DNS Provider

    +

    A typical way to start on, e.g. a CoreDNS provider, would be to add a coredns.go to the providers package and implement the interface methods. Then you would have to register your provider under a name in main.go, e.g. coredns, and would be able to trigger it’s functions via setting --provider=coredns.

    +

    Note, how your provider doesn’t need to know anything about where the DNS records come from, nor does it have to figure out the difference between the current and the desired state, it merely executes the actions calculated by the plan.

    +

    Running GitHub Actions locally

    +

    You can also extend the CI workflow which is currently implemented as GitHub Action within the workflow folder.
    +In order to test your changes before committing you can leverage act to run the GitHub Action locally.

    +

    Follow the installation instructions in the nektos/act README.md.
    +Afterwards just run act within the root folder of the project.

    +

    For further usage of act refer to its documentation.

    + +
    +
    + + + Last update: + August 1, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/contributing/sources-and-providers/index.html b/v0.13.6/contributing/sources-and-providers/index.html new file mode 100644 index 0000000000..e8c859df37 --- /dev/null +++ b/v0.13.6/contributing/sources-and-providers/index.html @@ -0,0 +1,2034 @@ + + + + + + + + + + + + + + + + + + Sources and Providers - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + +
    +
    +
    + + + +
    +
    +
    + + +
    +
    + + + + + + + + +

    Sources and Providers

    +

    ExternalDNS supports swapping out endpoint sources and DNS providers and both sides are pluggable. There currently exist three sources and four provider implementations.

    +

    Sources

    +

    Sources are an abstraction over any kind of source of desired Endpoints, e.g.:
    +* a list of Service objects from Kubernetes
    +* a random list for testing purposes
    +* an aggregated list of multiple nested sources

    +

    The Source interface has a single method called Endpoints that should return all desired Endpoint objects as a flat list.

    +
    type Source interface {
    +    Endpoints() ([]*endpoint.Endpoint, error)
    +}
    +
    +

    All sources live in package source.

    +
      +
    • ServiceSource: collects all Services that have an external IP and returns them as Endpoint objects. The desired DNS name corresponds to an annotation set on the Service or is compiled from the Service attributes via the FQDN Go template string.
    • +
    • IngressSource: collects all Ingresses that have an external IP and returns them as Endpoint objects. The desired DNS name corresponds to the host rules defined in the Ingress object.
    • +
    • IstioGatewaySource: collects all Istio Gateways and returns them as Endpoint objects. The desired DNS name corresponds to the hosts listed within the servers spec of each Gateway object.
    • +
    • ContourIngressRouteSource: collects all Contour IngressRoutes and returns them as Endpoint objects. The desired DNS name corresponds to the virtualhost.fqdn listed within the spec of each IngressRoute object.
    • +
    • FakeSource: returns a random list of Endpoints for the purpose of testing providers without having access to a Kubernetes cluster.
    • +
    • ConnectorSource: returns a list of Endpoint objects which are served by a tcp server configured through connector-source-server flag.
    • +
    • CRDSource: returns a list of Endpoint objects sourced from the spec of CRD objects. For more details refer to CRD source documentation.
    • +
    • EmptySource: returns an empty list of Endpoint objects for the purpose of testing and cleaning out entries.
    • +
    +

    Providers

    +

    Providers are an abstraction over any kind of sink for desired Endpoints, e.g.:
    +* storing them in Google Cloud DNS
    +* printing them to stdout for testing purposes
    +* fanning out to multiple nested providers

    +

    The Provider interface has two methods: Records and ApplyChanges. Records should return all currently existing DNS records converted to Endpoint objects as a flat list. Upon receiving a change set (via an object of plan.Changes), ApplyChanges should translate these to the provider specific actions in order to persist them in the provider’s storage.

    +
    type Provider interface {
    +    Records() ([]*endpoint.Endpoint, error)
    +    ApplyChanges(changes *plan.Changes) error
    +}
    +
    +

    The interface tries to be generic and assumes a flat list of records for both functions. However, many providers scope records into zones. Therefore, the provider implementation has to do some extra work to return that flat list. For instance, the AWS provider fetches the list of all hosted zones before it can return or apply the list of records. If the provider has no concept of zones or if it makes sense to cache the list of hosted zones it is happily allowed to do so. Furthermore, the provider should respect the --domain-filter flag to limit the affected records by a domain suffix. For instance, the AWS provider filters out all hosted zones that doesn’t match that domain filter.

    +

    All providers live in package provider.

    +
      +
    • GoogleProvider: returns and creates DNS records in Google Cloud DNS
    • +
    • AWSProvider: returns and creates DNS records in AWS Route 53
    • +
    • AzureProvider: returns and creates DNS records in Azure DNS
    • +
    • InMemoryProvider: Keeps a list of records in local memory
    • +
    +

    Usage

    +

    You can choose any combination of sources and providers on the command line. Given a cluster on AWS you would most likely want to use the Service and Ingress Source in combination with the AWS provider. Service + InMemory is useful for testing your service collecting functionality, whereas Fake + Google is useful for testing that the Google provider behaves correctly, etc.

    + +
    +
    + + + Last update: + November 5, 2020 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/faq/index.html b/v0.13.6/faq/index.html new file mode 100644 index 0000000000..3d37b1c6a7 --- /dev/null +++ b/v0.13.6/faq/index.html @@ -0,0 +1,2637 @@ + + + + + + + + + + + + + + + + + + FAQ - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + +
    +
    +
    + + + +
    +
    +
    + + +
    +
    + + + + + + + + +

    Frequently asked questions

    +

    How is ExternalDNS useful to me?

    +

    You’ve probably created many deployments. Typically, you expose your deployment to the Internet by creating a Service with type=LoadBalancer. Depending on your environment, this usually assigns a random publicly available endpoint to your service that you can access from anywhere in the world. On Google Kubernetes Engine, this is a public IP address:

    +
    $ kubectl get svc
    +NAME      CLUSTER-IP     EXTERNAL-IP     PORT(S)        AGE
    +nginx     10.3.249.226   35.187.104.85   80:32281/TCP   1m
    +
    +

    But dealing with IPs for service discovery isn’t nice, so you register this IP with your DNS provider under a better name—most likely, one that corresponds to your service name. If the IP changes, you update the DNS record accordingly.

    +

    Those times are over! ExternalDNS takes care of that last step for you by keeping your DNS records synchronized with your external entry points.

    +

    ExternalDNS’ usefulness also becomes clear when you use Ingresses to allow external traffic into your cluster. Via Ingress, you can tell Kubernetes to route traffic to different services based on certain HTTP request attributes, e.g. the Host header:

    +
    $ kubectl get ing
    +NAME         HOSTS                                      ADDRESS         PORTS     AGE
    +entrypoint   frontend.example.org,backend.example.org   35.186.250.78   80        1m
    +
    +

    But there’s nothing that actually makes clients resolve those hostnames to the Ingress’ IP address. Again, you normally have to register each entry with your DNS provider. Only if you’re lucky can you use a wildcard, like in the example above.

    +

    ExternalDNS can solve this for you as well.

    +

    Which DNS providers are supported?

    +

    Please check the provider status table for the list of supported providers and their status.

    +

    As stated in the README, we are currently looking for stable maintainers for those providers, to ensure that bugfixes and new features will be available for all of those.

    +

    Which Kubernetes objects are supported?

    +

    Services exposed via type=LoadBalancer, type=ExternalName, type=NodePort, and for the hostnames defined in Ingress objects as well as headless hostPort services.

    +

    How do I specify a DNS name for my Kubernetes objects?

    +

    There are three sources of information for ExternalDNS to decide on DNS name. ExternalDNS will pick one in order as listed below:

    +
      +
    1. +

      For ingress objects ExternalDNS will create a DNS record based on the hosts specified for the ingress object, as well as the external-dns.alpha.kubernetes.io/hostname annotation. For services ExternalDNS will look for the annotation external-dns.alpha.kubernetes.io/hostname on the service and use the loadbalancer IP, it also will look for the annotation external-dns.alpha.kubernetes.io/internal-hostname on the service and use the service IP.

      +
        +
      • For ingresses, you can optionally force ExternalDNS to create records based on either the hosts specified or the external-dns.alpha.kubernetes.io/hostname annotation. This behavior is controlled by
        + setting the external-dns.alpha.kubernetes.io/ingress-hostname-source annotation on that ingress to either defined-hosts-only or annotation-only.
      • +
      +
    2. +
    3. +

      If compatibility mode is enabled (e.g. --compatibility={mate,molecule} flag), External DNS will parse annotations used by Zalando/Mate, wearemolecule/route53-kubernetes. Compatibility mode with Kops DNS Controller is planned to be added in the future.

      +
    4. +
    5. +

      If --fqdn-template flag is specified, e.g. --fqdn-template={{.Name}}.my-org.com, ExternalDNS will use service/ingress specifications for the provided template to generate DNS name.

      +
    6. +
    +

    Can I specify multiple global FQDN templates?

    +

    Yes, you can. Pass in a comma separated list to --fqdn-template. Beaware this will double (triple, etc) the amount of DNS entries based on how many services, ingresses and so on you have and will get you faster towards the API request limit of your DNS provider.

    +

    Which Service and Ingress controllers are supported?

    +

    Regarding Services, we’ll support the OSI Layer 4 load balancers that Kubernetes creates on AWS and Google Kubernetes Engine, and possibly other clusters running on Google Compute Engine.

    +

    Regarding Ingress, we’ll support:
    +* Google’s Ingress Controller on GKE that integrates with their Layer 7 load balancers (GLBC)
    +* nginx-ingress-controller v0.9.x with a fronting Service
    +* Zalando’s AWS Ingress controller, based on AWS ALBs and Skipper
    +* Traefik
    + * version 1.7, when kubernetes.ingressEndpoint is configured (kubernetes.ingressEndpoint.useDefaultPublishedService in the Helm chart)
    + * versions >=2.0, when providers.kubernetesIngress.ingressEndpoint is configured (providers.kubernetesIngress.publishedService.enabled is set to true in the new Helm chart)

    +

    Are other Ingress Controllers supported?

    +

    For Ingress objects, ExternalDNS will attempt to discover the target hostname of the relevant Ingress Controller automatically. If you are using an Ingress Controller that is not listed above you may have issues with ExternalDNS not discovering Endpoints and consequently not creating any DNS records. As a workaround, it is possible to force create an Endpoint by manually specifying a target host/IP for the records to be created by setting the annotation external-dns.alpha.kubernetes.io/target in the Ingress object.

    +

    Another reason you may want to override the ingress hostname or IP address is if you have an external mechanism for handling failover across ingress endpoints. Possible scenarios for this would include using keepalived-vip to manage failover faster than DNS TTLs might expire.

    +

    Note that if you set the target to a hostname, then a CNAME record will be created. In this case, the hostname specified in the Ingress object’s annotation must already exist. (i.e. you have a Service resource for your Ingress Controller with the external-dns.alpha.kubernetes.io/hostname annotation set to the same value.)

    +

    What about other projects similar to ExternalDNS?

    +

    ExternalDNS is a joint effort to unify different projects accomplishing the same goals, namely:

    + +

    We strive to make the migration from these implementations a smooth experience. This means that, for some time, we’ll support their annotation semantics in ExternalDNS and allow both implementations to run side-by-side. This enables you to migrate incrementally and slowly phase out the other implementation.

    +

    How does it work with other implementations and legacy records?

    +

    ExternalDNS will allow you to opt into any Services and Ingresses that you want it to consider, by an annotation. This way, it can co-exist with other implementations running in the same cluster if they also support this pattern. However, we’ll most likely declare ExternalDNS to be the default implementation. This means that ExternalDNS will consider Services and Ingresses that don’t specifically declare which controller they want to be processed by; this is similar to the ingress.class annotation on GKE.

    +

    I’m afraid you will mess up my DNS records!

    +

    Since v0.3, ExternalDNS can be configured to use an ownership registry. When this option is enabled, ExternalDNS will keep track of which records it has control over, and will never modify any records over which it doesn’t have control. This is a fundamental requirement to operate ExternalDNS safely when there might be other actors creating DNS records in the same target space.

    +

    For now ExternalDNS uses TXT records to label owned records, and there might be other alternatives coming in the future releases.

    +

    Does anyone use ExternalDNS in production?

    +

    Yes, multiple companies are using ExternalDNS in production. Zalando, as an example, has been using it in production since its v0.3 release, mostly using the AWS provider.

    +

    How can we start using ExternalDNS?

    +

    Check out the following descriptive tutorials on how to run ExternalDNS in GKE and AWS or any other supported provider.

    +

    Why is ExternalDNS only adding a single IP address in Route 53 on AWS when using the nginx-ingress-controller? How do I get it to use the FQDN of the ELB assigned to my nginx-ingress-controller Service instead?

    +

    By default the nginx-ingress-controller assigns a single IP address to an Ingress resource when it’s created. ExternalDNS uses what’s assigned to the Ingress resource, so it too will use this single IP address when adding the record in Route 53.

    +

    In most AWS deployments, you’ll instead want the Route 53 entry to be the FQDN of the ELB that is assigned to the nginx-ingress-controller Service. To accomplish this, when you create the nginx-ingress-controller Deployment, you need to provide the --publish-service option to the /nginx-ingress-controller executable under args. Once this is deployed new Ingress resources will get the ELB’s FQDN and ExternalDNS will use the same when creating records in Route 53.

    +

    According to the nginx-ingress-controller docs the value you need to provide --publish-service is:

    +
    +

    Service fronting the ingress controllers. Takes the form namespace/name. The controller will set the endpoint records on the ingress objects to reflect those on the service.

    +
    +

    For example if your nginx-ingress-controller Service’s name is nginx-ingress-controller-svc and it’s in the default namespace the start of your resource YAML might look like the following. Note the second to last line.

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: nginx-ingress-controller
    +spec:
    +  replicas: 1
    +  selector:
    +    matchLabels:
    +      app: nginx-ingress
    +  template:
    +    metadata:
    +      labels:
    +        app: nginx-ingress
    +    spec:
    +      hostNetwork: false
    +      containers:
    +        - name: nginx-ingress-controller
    +          image: "gcr.io/google_containers/nginx-ingress-controller:0.9.0-beta.11"
    +          imagePullPolicy: "IfNotPresent"
    +          args:
    +            - /nginx-ingress-controller
    +            - --default-backend-service={your-backend-service}
    +            - --publish-service=default/nginx-ingress-controller-svc
    +            - --configmap={your-configmap}
    +
    +

    I have a Service/Ingress but it’s ignored by ExternalDNS. Why?

    +

    ExternalDNS can be configured to only use Services or Ingresses as source. In case Services or Ingresses seem to be ignored in your setup, consider checking how the flag --source was configured when deployed. For reference, see the issue https://github.com/kubernetes-sigs/external-dns/issues/267.

    +

    I’m using an ELB with TXT registry but the CNAME record clashes with the TXT record. How to avoid this?

    +

    CNAMEs cannot co-exist with other records, therefore you can use the --txt-prefix flag which makes sure to create a TXT record with a name following the pattern prefix.<CNAME record>. For reference, see the issue https://github.com/kubernetes-sigs/external-dns/issues/262.

    +

    Can I force ExternalDNS to create CNAME records for ELB/ALB?

    +

    The default logic is: when a target looks like an ELB/ALB, ExternalDNS will create ALIAS records for it.
    +Under certain circumstances you want to force ExternalDNS to create CNAME records instead. If you want to do that, start ExternalDNS with the --aws-prefer-cname flag.

    +

    Why should I want to force ExternalDNS to create CNAME records for ELB/ALB? Some motivations of users were:

    +
    +

    “Our hosted zones records are synchronized with our enterprise DNS. The record type ALIAS is an AWS proprietary record type and AWS allows you to set a DNS record directly on AWS resources. Since this is not a DNS RfC standard and therefore can not be transferred and created in our enterprise DNS. So we need to force CNAME creation instead.”

    +
    +

    or

    +
    +

    “In case of ALIAS if we do nslookup with domain name, it will return only IPs of ELB. So it is always difficult for us to locate ELB in AWS console to which domain is pointing. If we configure it with CNAME it will return exact ELB CNAME, which is more helpful.!”

    +
    +

    Which permissions do I need when running ExternalDNS on a GCE or GKE node.

    +

    You need to add either https://www.googleapis.com/auth/ndev.clouddns.readwrite or https://www.googleapis.com/auth/cloud-platform on your instance group’s scope.

    +

    What metrics can I get from ExternalDNS and what do they mean?

    +

    ExternalDNS exposes 2 types of metrics: Sources and Registry errors.

    +

    Sources are mostly Kubernetes API objects. Examples of source errors may be connection errors to the Kubernetes API server itself or missing RBAC permissions. It can also stem from incompatible configuration in the objects itself like invalid characters, processing a broken fqdnTemplate, etc.

    +

    Registry errors are mostly Provider errors, unless there’s some coding flaw in the registry package. Provider errors often arise due to accessing their APIs due to network or missing cloud-provider permissions when reading records. When applying a changeset, errors will arise if the changeset applied is incompatible with the current state.

    +

    In case of an increased error count, you could correlate them with the http_request_duration_seconds{handler="instrumented_http"} metric which should show increased numbers for status codes 4xx (permissions, configuration, invalid changeset) or 5xx (apiserver down).

    +

    You can use the host label in the metric to figure out if the request was against the Kubernetes API server (Source errors) or the DNS provider API (Registry/Provider errors).

    +

    Here is the full list of available metrics provided by ExternalDNS:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameDescriptionType
    external_dns_controller_last_sync_timestamp_secondsTimestamp of last successful sync with the DNS providerGauge
    external_dns_registry_endpoints_totalNumber of Endpoints in all sourcesGauge
    external_dns_registry_errors_totalNumber of Registry errorsCounter
    external_dns_source_endpoints_totalNumber of Endpoints in the registryGauge
    external_dns_source_errors_totalNumber of Source errorsCounter
    external_dns_controller_verified_aaaa_recordsNumber of DNS AAAA-records that exists both in source and registryGauge
    external_dns_controller_verified_a_recordsNumber of DNS A-records that exists both in source and registryGauge
    external_dns_registry_aaaa_recordsNumber of AAAA records in registryGauge
    external_dns_registry_a_recordsNumber of A records in registryGauge
    external_dns_source_aaaa_recordsNumber of AAAA records in sourceGauge
    external_dns_source_a_recordsNumber of A records in sourceGauge
    +

    How can I run ExternalDNS under a specific GCP Service Account, e.g. to access DNS records in other projects?

    +

    Have a look at https://github.com/linki/mate/blob/v0.6.2/examples/google/README.md#permissions

    +

    How do I configure multiple Sources via environment variables? (also applies to domain filters)

    +

    Separate the individual values via a line break. The equivalent of --source=service --source=ingress would be service\ningress. However, it can be tricky do define that depending on your environment. The following examples work (zsh):

    +

    Via docker:

    +
    $ docker run \
    +  -e EXTERNAL_DNS_SOURCE=$'service\ningress' \
    +  -e EXTERNAL_DNS_PROVIDER=google \
    +  -e EXTERNAL_DNS_DOMAIN_FILTER=$'foo.com\nbar.com' \
    +  registry.k8s.io/external-dns/external-dns:v0.13.5
    +time="2017-08-08T14:10:26Z" level=info msg="config: &{APIServerURL: KubeConfig: Sources:[service ingress] Namespace: ...
    +
    +

    Locally:

    +
    $ export EXTERNAL_DNS_SOURCE=$'service\ningress'
    +$ external-dns --provider=google
    +INFO[0000] config: &{APIServerURL: KubeConfig: Sources:[service ingress] Namespace: ...
    +
    +
    $ EXTERNAL_DNS_SOURCE=$'service\ningress' external-dns --provider=google
    +INFO[0000] config: &{APIServerURL: KubeConfig: Sources:[service ingress] Namespace: ...
    +
    +

    In a Kubernetes manifest:

    +
    spec:
    +  containers:
    +  - name: external-dns
    +    args:
    +    - --provider=google
    +    env:
    +    - name: EXTERNAL_DNS_SOURCE
    +      value: "service\ningress"
    +
    +

    Or preferably:

    +
    spec:
    +  containers:
    +  - name: external-dns
    +    args:
    +    - --provider=google
    +    env:
    +    - name: EXTERNAL_DNS_SOURCE
    +      value: |-
    +        service
    +        ingress
    +
    +

    Running an internal and external dns service

    +

    Sometimes you need to run an internal and an external dns service.
    +The internal one should provision hostnames used on the internal network (perhaps inside a VPC), and the external one to expose DNS to the internet.

    +

    To do this with ExternalDNS you can use the --ingress-class flag to specifically tie an instance of ExternalDNS to an instance of a ingress controller.
    +Let’s assume you have two ingress controllers, internal and external.
    +You can then start two ExternalDNS providers, one with --ingress-class=internal and one with --ingress-class=external.

    +

    If you need to search for multiple ingress classes, you can specify the flag multiple times, like so:
    +--ingress-class=internal --ingress-class=external.

    +

    The --ingress-class flag will check both the spec.ingressClassName field and the deprecated kubernetes.io/ingress.class annotation.
    +The spec.ingressClassName tasks precedence over the annotation if both are supplied.

    +

    Backward compatibility

    +

    The previous --annotation-filter flag can still be used to restrict which objects ExternalDNS considers; for example, --annotation-filter=kubernetes.io/ingress.class in (public,dmz).

    +

    However, beware when using annotation filters with multiple sources, e.g. --source=service --source=ingress, since --annotation-filter will filter every given source object.
    +If you need to use annotation filters against a specific source you have to run a separated external dns service containing only the wanted --source and --annotation-filter.

    +

    Note: the --ingress-class flag cannot be used at the same time as the --annotation-filter=kubernetes.io/ingress.class in (...) flag; if you do this an error will be raised.

    +

    Performance considerations

    +

    Filtering based on ingress class name or annotations means that the external-dns controller will receive all resources of that kind and then filter on the client-side.
    +In larger clusters with many resources which change frequently this can cause performance issues.
    +If only some resources need to be managed by an instance of external-dns then label filtering can be used instead of ingress class filtering (or legacy annotation filtering).
    +This means that only those resources which match the selector specified in --label-filter will be passed to the controller.

    +

    How do I specify that I want the DNS record to point to either the Node’s public or private IP when it has both?

    +

    If your Nodes have both public and private IP addresses, you might want to write DNS records with one or the other.
    +For example, you may want to write a DNS record in a private zone that resolves to your Nodes’ private IPs so that traffic never leaves your private network.

    +

    To accomplish this, set this annotation on your service: external-dns.alpha.kubernetes.io/access=private
    +Conversely, to force the public IP: external-dns.alpha.kubernetes.io/access=public

    +

    If this annotation is not set, and the node has both public and private IP addresses, then the public IP will be used by default.

    +

    Some loadbalancer implementations assign multiple IP addresses as external addresses. You can filter the generated targets by their networks
    +using --target-net-filter=10.0.0.0/8 or --exclude-target-net=10.0.0.0/8.

    +

    Can external-dns manage(add/remove) records in a hosted zone which is setup in different AWS account?

    +

    Yes, give it the correct cross-account/assume-role permissions and use the --aws-assume-role flag https://github.com/kubernetes-sigs/external-dns/pull/524#issue-181256561

    +

    How do I provide multiple values to the annotation external-dns.alpha.kubernetes.io/hostname?

    +

    Separate them by ,.

    +

    Are there official Docker images provided?

    +

    When we tag a new release, we push a container image to the Kubernetes projects official container registry with the following name:

    +
    registry.k8s.io/external-dns/external-dns
    +
    +

    As tags, you use the external-dns release of choice(i.e. v0.7.6). A latest tag is not provided in the container registry.

    +

    If you wish to build your own image, you can use the provided .ko.yaml as a starting point.

    +

    Which architectures are supported?

    +

    From v0.7.5 on we support amd64, arm32v7 and arm64v8. This means that you can run ExternalDNS on a Kubernetes cluster backed by Rasperry Pis or on ARM instances in the cloud as well as more traditional machines backed by amd64 compatible CPUs.

    +

    Which operating systems are supported?

    +

    At the time of writing we only support GNU/linux and we have no plans of supporting Windows or other operating systems.

    +

    Why am I seeing time out errors even though I have connectivity to my cluster?

    +

    If you’re seeing an error such as this:
    +

    FATA[0060] failed to sync cache: timed out waiting for the condition
    +

    +

    You may not have the correct permissions required to query all the necessary resources in your kubernetes cluster. Specifically, you may be running in a namespace that you don’t have these permissions in. By default, commands are run against the default namespace. Try changing this to your particular namespace to see if that fixes the issue.

    + +
    +
    + + + Last update: + August 1, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/img/external-dns.png b/v0.13.6/img/external-dns.png new file mode 100644 index 0000000000000000000000000000000000000000..0ee8e0753172768e52a40fe936674e050f80a9ae GIT binary patch literal 256840 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

    Ci0>wh=Lg}%=7O{fQAv1|mpQSH_IQ6*s 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%0D3vJ)FU4)C* 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μ#uZ?a&eUOIbrd5C8XA)Y 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

    NYF&`qrmW5=5U`lK)pJ6^v{ZWZ|pj4N>BEvul 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 literal 0 HcmV?d00001 diff --git a/v0.13.6/index.html b/v0.13.6/index.html new file mode 100644 index 0000000000..10a37e6b78 --- /dev/null +++ b/v0.13.6/index.html @@ -0,0 +1,2501 @@ + + + + + + + + + + + + + + + + + + external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

    +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + + + + + + + + + + + + +
    +
    + + + + + + + + +

    + ExternalDNS +

    + +

    ExternalDNS

    +

    Build Status Coverage Status GitHub release go-doc Go Report Card ExternalDNS docs

    +

    ExternalDNS synchronizes exposed Kubernetes Services and Ingresses with DNS providers.

    +

    What It Does

    +

    Inspired by Kubernetes DNS, Kubernetes’ cluster-internal DNS server, ExternalDNS makes Kubernetes resources discoverable via public DNS servers. Like KubeDNS, it retrieves a list of resources (Services, Ingresses, etc.) from the Kubernetes API to determine a desired list of DNS records. Unlike KubeDNS, however, it’s not a DNS server itself, but merely configures other DNS providers accordingly—e.g. AWS Route 53 or Google Cloud DNS.

    +

    In a broader sense, ExternalDNS allows you to control DNS records dynamically via Kubernetes resources in a DNS provider-agnostic way.

    +

    The FAQ contains additional information and addresses several questions about key concepts of ExternalDNS.

    +

    To see ExternalDNS in action, have a look at this video or read this blogpost.

    +

    The Latest Release

    +

    ExternalDNS allows you to keep selected zones (via --domain-filter) synchronized with Ingresses and Services of type=LoadBalancer and nodes in various DNS providers:
    +* Google Cloud DNS
    +* AWS Route 53
    +* AWS Cloud Map
    +* AzureDNS
    +* BlueCat
    +* Civo
    +* CloudFlare
    +* RcodeZero
    +* DigitalOcean
    +* DNSimple
    +* Infoblox
    +* Dyn
    +* OpenStack Designate
    +* PowerDNS
    +* CoreDNS
    +* Exoscale
    +* Oracle Cloud Infrastructure DNS
    +* Linode DNS
    +* RFC2136
    +* NS1
    +* TransIP
    +* VinylDNS
    +* Vultr
    +* OVH
    +* Scaleway
    +* Akamai Edge DNS
    +* GoDaddy
    +* Gandi
    +* ANS Group SafeDNS
    +* IBM Cloud DNS
    +* TencentCloud PrivateDNS
    +* TencentCloud DNSPod
    +* Plural
    +* Pi-hole

    +

    ExternalDNS is, by default, aware of the records it is managing, therefore it can safely manage non-empty hosted zones. We strongly encourage you to set --txt-owner-id to a unique value that doesn’t change for the lifetime of your cluster. You might also want to run ExternalDNS in a dry run mode (--dry-run flag) to see the changes to be submitted to your DNS Provider API.

    +

    Note that all flags can be replaced with environment variables; for instance,
    +--dry-run could be replaced with EXTERNAL_DNS_DRY_RUN=1.

    +

    Status of providers

    +

    ExternalDNS supports multiple DNS providers which have been implemented by the ExternalDNS contributors. Maintaining all of those in a central repository is a challenge and we have limited resources to test changes. This means that it is very hard to test all providers for possible regressions and, as written in the Contributing section, we encourage contributors to step in as maintainers for the individual providers and help by testing the integrations.

    +

    End-to-end testing of ExternalDNS is currently
    +performed
    +in the separate
    +kubernetes-on-aws
    +repository.

    +

    We define the following stability levels for providers:

    +
      +
    • Stable: Used for smoke tests before a release, used in production and maintainers are active.
    • +
    • Beta: Community supported, well tested, but maintainers have no access to resources to execute integration tests on the real platform and/or are not using it in production.
    • +
    • Alpha: Community provided with no support from the maintainers apart from reviewing PRs.
    • +
    +

    The following table clarifies the current status of the providers according to the aforementioned stability levels:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ProviderStatusMaintainers
    Google Cloud DNSStable
    AWS Route 53Stable
    AWS Cloud MapBeta
    Akamai Edge DNSBeta
    AzureDNSBeta
    BlueCatAlpha@seanmalloy @vinny-sabatini
    CivoAlpha@alejandrojnm
    CloudFlareBeta
    RcodeZeroAlpha
    DigitalOceanAlpha
    DNSimpleAlpha
    InfobloxAlpha@saileshgiri
    DynAlpha
    OpenStack DesignateAlpha
    PowerDNSAlpha
    CoreDNSAlpha
    ExoscaleAlpha
    Oracle Cloud Infrastructure DNSAlpha
    Linode DNSAlpha
    RFC2136Alpha
    NS1Alpha
    TransIPAlpha
    VinylDNSAlpha
    RancherDNSAlpha
    OVHAlpha
    Scaleway DNSAlpha@Sh4d1
    VultrAlpha
    UltraDNSAlpha
    GoDaddyAlpha
    GandiAlpha@packi
    SafeDNSAlpha@assureddt
    IBMCloudAlpha@hughhuangzh
    TencentCloudAlpha@Hyzhou
    PluralAlpha@michaeljguarino
    Pi-holeAlpha@tinyzimmer
    +

    Kubernetes version compatibility

    +

    A breaking change was added in external-dns v0.10.0.

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    ExternalDNS<= 0.9.x>= 0.10.0
    Kubernetes <= 1.18✅❌
    Kubernetes >= 1.19 and <= 1.21✅✅
    Kubernetes >= 1.22❌✅
    +

    Running ExternalDNS:

    +

    The are two ways of running ExternalDNS:

    +
      +
    • Deploying to a Cluster
    • +
    • Running Locally
    • +
    +

    Deploying to a Cluster

    +

    The following tutorials are provided:

    + +

    Running Locally

    +

    See the contributor guide for details on compiling
    +from source.

    +

    Setup Steps

    +

    Next, run an application and expose it via a Kubernetes Service:

    +
    kubectl run nginx --image=nginx --port=80
    +kubectl expose pod nginx --port=80 --target-port=80 --type=LoadBalancer
    +
    +

    Annotate the Service with your desired external DNS name. Make sure to change example.org to your domain.

    +
    kubectl annotate service nginx "external-dns.alpha.kubernetes.io/hostname=nginx.example.org."
    +
    +

    Optionally, you can customize the TTL value of the resulting DNS record by using the external-dns.alpha.kubernetes.io/ttl annotation:

    +
    kubectl annotate service nginx "external-dns.alpha.kubernetes.io/ttl=10"
    +
    +

    For more details on configuring TTL, see here.

    +

    Use the internal-hostname annotation to create DNS records with ClusterIP as the target.

    +
    kubectl annotate service nginx "external-dns.alpha.kubernetes.io/internal-hostname=nginx.internal.example.org."
    +
    +

    If the service is not of type Loadbalancer you need the –publish-internal-services flag.

    +

    Locally run a single sync loop of ExternalDNS.

    +
    external-dns --txt-owner-id my-cluster-id --provider google --google-project example-project --source service --once --dry-run
    +
    +

    This should output the DNS records it will modify to match the managed zone with the DNS records you desire. It also assumes you are running in the default namespace. See the FAQ for more information regarding namespaces.

    +

    Note: TXT records will have the my-cluster-id value embedded. Those are used to ensure that ExternalDNS is aware of the records it manages.

    +

    Once you’re satisfied with the result, you can run ExternalDNS like you would run it in your cluster: as a control loop, and not in dry-run mode:

    +
    external-dns --txt-owner-id my-cluster-id --provider google --google-project example-project --source service
    +
    +

    Check that ExternalDNS has created the desired DNS record for your Service and that it points to its load balancer’s IP. Then try to resolve it:

    +
    dig +short nginx.example.org.
    +104.155.60.49
    +
    +

    Now you can experiment and watch how ExternalDNS makes sure that your DNS records are configured as desired. Here are a couple of things you can try out:
    +* Change the desired hostname by modifying the Service’s annotation.
    +* Recreate the Service and see that the DNS record will be updated to point to the new load balancer IP.
    +* Add another Service to create more DNS records.
    +* Remove Services to clean up your managed zone.

    +

    The tutorials section contains examples, including Ingress resources, and shows you how to set up ExternalDNS in different environments such as other cloud providers and alternative Ingress controllers.

    +

    Note

    +

    If using a txt registry and attempting to use a CNAME the --txt-prefix must be set to avoid conflicts. Changing --txt-prefix will result in lost ownership over previously created records.

    +

    If externalIPs list is defined for a LoadBalancer service, this list will be used instead of an assigned load balancer IP to create a DNS record. It’s useful when you run bare metal Kubernetes clusters behind NAT or in a similar setup, where a load balancer IP differs from a public IP (e.g. with MetalLB).

    +

    Contributing

    +

    Are you interested in contributing to external-dns? We, the maintainers and community, would love your
    +suggestions, contributions, and help! Also, the maintainers can be contacted at any time to learn more
    +about how to get involved.

    +

    We also encourage ALL active community participants to act as if they are maintainers, even if you don’t have
    +“official” write permissions. This is a community effort, we are here to serve the Kubernetes community. If you
    +have an active interest and you want to get involved, you have real power! Don’t assume that the only people who
    +can get things done around here are the “maintainers”. We also would love to add more “official” maintainers, so
    +show us what you can do!

    +

    The external-dns project is currently in need of maintainers for specific DNS providers. Ideally each provider
    +would have at least two maintainers. It would be nice if the maintainers run the provider in production, but it
    +is not strictly required. Provider listed here
    +that do not have a maintainer listed are in need of assistance.

    +

    Read the contributing guidelines and have a look at the contributing docs to learn about building the project, the project structure, and the purpose of each package.

    +

    For an overview on how to write new Sources and Providers check out Sources and Providers.

    +

    Heritage

    +

    ExternalDNS is an effort to unify the following similar projects in order to bring the Kubernetes community an easy and predictable way of managing DNS records across cloud providers based on their Kubernetes resources:

    + +

    User Demo How-To Blogs and Examples

    + + +
    +
    + + + Last update: + September 6, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/initial-design/index.html b/v0.13.6/initial-design/index.html new file mode 100644 index 0000000000..7df12c3b20 --- /dev/null +++ b/v0.13.6/initial-design/index.html @@ -0,0 +1,2254 @@ + + + + + + + + + + + + + + + + + + Initial Design - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + +
    +
    +
    + + + +
    +
    +
    + + +
    +
    + + + + + + + + +

    Proposal: Design of External DNS

    +

    Background

    +

    Project proposal

    +

    Initial discussion

    +

    This document describes the initial design proposal.

    +

    External DNS is purposed to fill the existing gap of creating DNS records for Kubernetes resources. While there exist alternative solutions, this project is meant to be a standard way of managing DNS records for Kubernetes. The current project is a fusion of the following projects and driven by its maintainers:

    +
      +
    1. Kops DNS Controller
    2. +
    3. Mate
    4. +
    5. wearemolecule/route53-kubernetes
    6. +
    +

    Example use case:

    +

    User runs kubectl create -f ingress.yaml, this will create an ingress as normal.
    +Typically the user would then have to manually create a DNS record pointing the ingress endpoint
    +If the external-dns controller is running on the cluster, it could automatically configure the DNS records instead, by observing the host attribute in the ingress object.

    +

    Goals

    +
      +
    1. Support AWS Route53 and Google Cloud DNS providers
    2. +
    3. DNS for Kubernetes services(type=Loadbalancer) and Ingress
    4. +
    5. Create/update/remove records as according to Kubernetes resources state
    6. +
    7. It should address main requirements and support main features of the projects mentioned above
    8. +
    +

    Design

    +

    Extensibility

    +

    New cloud providers should be easily pluggable. Initially only AWS/Google platforms are supported. However, in the future we are planning to incorporate CoreDNS and Azure DNS as possible DNS providers

    +

    Configuration

    +

    DNS records will be automatically created in multiple situations:
    +1. Setting spec.rules.host on an ingress object.
    +2. Setting spec.tls.hosts on an ingress object.
    +3. Adding the annotation external-dns.alpha.kubernetes.io/hostname on an ingress object.
    +4. Adding the annotation external-dns.alpha.kubernetes.io/hostname on a type=LoadBalancer service object.

    +

    Annotations

    +

    Record configuration should occur via resource annotations. Supported annotations:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Annotations
    Tagexternal-dns.alpha.kubernetes.io/controller
    DescriptionTells a DNS controller to process this service. This is useful when running different DNS controllers at the same time (or different versions of the same controller). The v1 implementation of dns-controller would look for service annotations dns-controller and dns-controller/v1 but not for mate/v1 or dns-controller/v2
    Defaultdns-controller
    Exampledns-controller/v1
    Requiredfalse
    Tagexternal-dns.alpha.kubernetes.io/hostname
    DescriptionFully qualified name of the desired record
    Defaultnone
    Examplefoo.example.org
    RequiredOnly for services. Ingress hostname is retrieved from spec.rules.host meta data on ingress
    +

    Compatibility

    +

    External DNS should be compatible with annotations used by three above mentioned projects. The idea is that resources created and tagged with annotations for other projects should continue to be valid and now managed by External DNS.

    +

    Mate

    +

    Mate does not require services/ingress to be tagged. Therefore, it is not safe to run both Mate and External-DNS simultaneously. The idea is that initial release (?) of External DNS will support Mate annotations, which indicates the hostname to be created. Therefore the switch should be simple.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Annotations
    Tagzalando.org/dnsname
    DescriptionHostname to be registered
    DefaultEmpty(falls back to template based approach)
    Examplefoo.example.org
    Requiredfalse
    +

    route53-kubernetes

    +

    It should be safe to run both route53-kubernetes and external-dns simultaneously. Since route53-kubernetes only looks at services with the label dns=route53 and does not support ingress there should be no collisions between annotations. If users desire to switch to external-dns they can run both controllers and migrate services over as they are able.

    +

    Ownership

    +

    External DNS should be responsible for the created records. Which means that the records should be tagged and only tagged records are viable for future deletion/update. It should not mess with pre-existing records created via other means.

    +

    Ownership via TXT records

    +

    Each record managed by External DNS is accompanied with a TXT record with a specific value to indicate that corresponding DNS record is managed by External DNS and it can be updated/deleted respectively. TXT records are limited to lifetimes of service/ingress objects and are created/deleted once k8s resources are created/deleted.

    + +
    +
    + + + Last update: + December 13, 2020 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/proposal/multi-target/index.html b/v0.13.6/proposal/multi-target/index.html new file mode 100644 index 0000000000..37120d560e --- /dev/null +++ b/v0.13.6/proposal/multi-target/index.html @@ -0,0 +1,2076 @@ + + + + + + + + + + + + + + + + + + Multiple Targets per hostname - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + +
    +
    +
    + + + +
    +
    +
    + + +
    +
    + + + + + + + + +

    Multiple Targets per hostname

    +

    (November 2017)

    +

    Purpose

    +

    One should be able to define multiple targets (IPs/Hostnames) in the same Kubernetes resource object and expect
    +ExternalDNS create DNS record(s) with a specified hostname and all targets. So far the connection between k8s resources (ingress/services) and DNS records
    +were not streamlined. This proposal aims to make the connection explicit, making k8s resources acquire or release certain DNS names. As long as the resource
    +ingress/service owns the record it can have multiple targets enable iff they are specified in the same resource.

    +

    Use cases

    +

    See https://github.com/kubernetes-sigs/external-dns/issues/239

    +

    Current behaviour

    +

    (as of the moment of writing)

    +

    Central piece of enabling multi-target is having consistent and correct behaviour in plan component in regards to how endpoints generated
    +from kubernetes resources are mapped to dns records. Current implementation of the plan has inconsistent behaviour in the following scenarios, all
    +of which must be resolved before multi-target support can be enabled in the provider implementations:

    +
      +
    1. +

      No records registered so far. Two different ingresses request same hostname but different targets, e.g. Ingress A: example.com -> 1.1.1.1 and Ingress B: example.com -> 2.2.2.2

      +
        +
      • Current Behaviour: both are added to the “Create” (records to be created) list and passed to Provider
      • +
      • Expected Behaviour: only one (random/ or according to predefined strategy) should be chosen and passed to Provider
      • +
      +

      NOTE: while this seems to go against multi-target support, this is done so no other resource can “hijack” already created DNS record. Multi targets are supported only
      +on per single resource basis

      +
    2. +
    3. +

      Now let’s say Ingress A was chosen and successfully created, but both ingress A and B are still there. So on next iteration ExternalDNS would see both again in the Desired list.

      +
        +
      • Current Behaviour: DNS record target will change to that of Ingress B.
      • +
      • Expected Behaviour: Ingress A should stay unchanged. Ingress B record is not created
      • +
      +
    4. +
    5. +

      DNS record for Ingress A was created but its target has changed. Ingress B is still there

      +
        +
      • Current Behaviour: Undetermined behaviour based on which ingress will be parsed last.
      • +
      • Expected Behaviour: DNS record should point to the new target specified in A. Ingress B should still be ignored.
      • +
      +

      NOTE: both 2. and 3. can be resolved if External DNS is aware which resource has already acquired DNS record

      +
    6. +
    7. +

      Ingress C has multiple targets: 1.1.1.1 and 2.2.2.2

      +
        +
      • Current Behaviour: Both targets are split into different endpoints and we end up in one of the cases above
      • +
      • Expected Behaviour: Endpoint should contain list of targets and treated as one ingress object.
      • +
      +
    8. +
    +

    Requirements and assumptions

    +

    For this feature to work we have to make sure that:

    +
      +
    1. DNS records are now owned by certain ingress/service resources. For External DNS it would mean that TXT records now
      +should store back-reference for the resource this record was created for, i.e. "heritage=external-dns,external-dns/resource=ingress/default/my-ingress-object-name"
    2. +
    3. +

      DNS records are updated only:

      +
        +
      • +

        If owning resource target list has changed

        +
      • +
      • +

        If owning resource record is not found in the desired list (meaning it was deleted), therefore it will now be owned by another record. So its target list will be updated

        +
      • +
      • +

        Changes related to other record properties (e.g. TTL)

        +
      • +
      +
    4. +
    5. +

      All of the issues described in Current Behaviour sections are resolved

      +
    6. +
    +

    Once Create/Update/Delete lists are calculated correctly (this is where conflicts based on requested DNS names are resolved) they are passed to provider, where
    +provider specific implementation will decide how to convert the structures into required formats. If DNS provider does not (or partially) support multi targets
    +then it is up to the provider to make sure that the change list of records passed to the DNS provider API is valid. TODO: explain best strategy.

    +

    Additionally see https://github.com/kubernetes-sigs/external-dns/issues/258

    +

    Implementation plan

    +

    Brief summary of open PRs and what they are trying to address:

    +

    PRs

    +
      +
    1. +

      https://github.com/kubernetes-sigs/external-dns/pull/243 - first attempt to add support for multiple targets. It is lagging far behind from tip

      +

      what it does: unfinished attempt to extend Endpoint struct, for it to allow multiple targets (essentially target string -> targets []string)

      +

      action: evaluate if rebasing makes sense, or we can just close it.

      +
    2. +
    3. +

      https://github.com/kubernetes-sigs/external-dns/pull/261 - attempt to rework plan to make it work correctly with multiple targets.

      +

      what it does : attempts to fix issues with plan described in Current Behaviour section above. Included tests reveal the current problem with plan

      +

      action: rebase on default branch and make necessary changes to satisfy requirements listed in this document including back-reference to owning record

      +
    4. +
    5. +

      https://github.com/kubernetes-sigs/external-dns/pull/326 - attempt to add multiple target support.

      +

      what it does: for each pair DNS Name + Record Type it aggregates all targets from the cluster and passes them to Provider. It adds basic support
      +for DO, Azure, Cloudflare, AWS, GCP, however those are not tested (?). (DNSSimple and Infoblox providers were not updated)

      +

      action: the plan logic will probably needs to be reworked, however the rest concerning support in Providers and extending Endpoint struct can be reused.
      +Rebase on default branch and add missing pieces. Depends on 2.

      +
    6. +
    +

    Related PRs: https://github.com/kubernetes-sigs/external-dns/pull/331/files, https://github.com/kubernetes-sigs/external-dns/pull/347/files - aiming at AWS Route53 weighted records.
    +These PRs should be considered after common agreement about the way to address multi-target support is achieved. Related discussion: https://github.com/kubernetes-sigs/external-dns/issues/196

    +

    How to proceed from here

    +

    The following steps are needed:
    +1. Make sure consensus regarding the approach is achieved via collaboration on the current document
    +2. Notify all PR (see above) authors about the agreed approach
    +3. Implementation:

    +
    a. `Plan` is working as expected - either based on #261 above or from scratch. `Plan` should be working correctly regardless of multi-target support
    +
    +b. Extensive testing making sure new `plan` does not introduce any breaking changes
    +
    +c. Change Endpoint struct to support multiple targets - based on #326 - integrate it with new `plan` @sethpollack
    +
    +d. Make sure new endpoint format can still be used in providers which have only partial support for multi targets ~~**TODO**: how ?~~ . This is to be done by simply using first target in the targets list.
    +
    +e. Add support for multi target which are already addressed in #326. It goes alongside c. and can be based on the same PR @sethpollack. New providers 
    +added since then should maintain same functionality.
    +
    +
      +
    1. Extensive testing on all providers before making new release
    2. +
    3. Update all related documentation and explain how multi targets are supported on per provider basis
    4. +
    5. Think of introducing weighted records (see PRs section above) and making them configurable.
    6. +
    +

    Open questions

    +
      +
    • Handling cases when ingress/service targets include both hostnames and IPs - postpone this until use cases occurs
    • +
    • “Weighted records scope”: https://github.com/kubernetes-sigs/external-dns/issues/196 - this should be considered once multi-target support is implemented
    • +
    + +
    +
    + + + Last update: + July 8, 2020 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/registry/dynamodb/index.html b/v0.13.6/registry/dynamodb/index.html new file mode 100644 index 0000000000..b4c9bc3e8d --- /dev/null +++ b/v0.13.6/registry/dynamodb/index.html @@ -0,0 +1,2055 @@ + + + + + + + + + + + + + + + + + + DynamoDB - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + +
    +
    +
    + + + +
    +
    +
    + + +
    +
    + + + + + + + + +

    The DynamoDB registry

    +

    The DynamoDB registry stores DNS record metadata in an AWS DynamoDB table.

    +

    The DynamoDB Table

    +

    By default, the DynamoDB registry stores data in the table named external-dns.
    +A different table may be specified using the --dynamodb-table flag.
    +A different region may be specified using the --dynamodb-region flag.

    +

    The table must have a partition (hash) key named k and string type.
    +The table must not have a sort (range) key.

    +

    IAM permissions

    +

    The ExternalDNS Role must be granted the following permissions:

    +
        {
    +      "Effect": "Allow",
    +      "Action": [
    +        "DynamoDB:DescribeTable",
    +        "DynamoDB:PartiQLDelete",
    +        "DynamoDB:PartiQLInsert",
    +        "DynamoDB:PartiQLUpdate",
    +        "DynamoDB:Scan"
    +      ],
    +      "Resource": [
    +        "arn:aws:dynamodb:*:*:table/external-dns"
    +      ]
    +    }
    +
    +

    The region and account ID may be specified explicitly specified instead of using wildcards.

    +

    Caching

    +

    The DynamoDB registry can optionally cache DNS records read from the provider. This can mitigate
    +rate limits imposed by the provider.

    +

    Caching is enabled by specifying a cache duration with the --txt-cache-interval flag.

    +

    Migration from TXT registry

    +

    If any ownership TXT records exist for the configured owner, the DynamoDB registry will migrate
    +the metadata therein to the DynamoDB table. If any such TXT records exist, any previous values for
    +--txt-prefix, --txt-suffix, --txt-wildcard-replacement, and --txt-encrypt-aes-key
    +must be supplied.

    +

    If TXT records are in the set of managed record types specified by --managed-record-types,
    +it will then delete the ownership TXT records on a subsequent reconciliation.

    + +
    +
    + + + Last update: + June 24, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/registry/registry/index.html b/v0.13.6/registry/registry/index.html new file mode 100644 index 0000000000..55075cf757 --- /dev/null +++ b/v0.13.6/registry/registry/index.html @@ -0,0 +1,1991 @@ + + + + + + + + + + + + + + + + + + About - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + +
    +
    +
    + + + +
    +
    +
    + + +
    +
    + + + + + + + + +

    Registries

    +

    A registry persists metadata pertaining to DNS records.

    +

    The most important metadata is the owning external-dns deployment.
    +This is specified using the --txt-owner-id flag, specifying a value unique to the
    +deployment of external-dns and which doesn’t change for the lifetime of the deployment.
    +Deployments in different clusters but sharing a DNS zone need to use different owner IDs.

    +

    The registry implementation is specified using the --registry flag.

    +

    Supported registries

    +
      +
    • txt (default) - Stores metadata in TXT records in the same provider.
    • +
    • dynamodb - Stores metadata in an AWS DynamoDB table.
    • +
    • noop - Passes metadata directly to the provider. For most providers, this means the metadata is not persisted.
    • +
    • aws-sd - Stores metadata in AWS Service Discovery. Only usable with the aws-sd provider.
    • +
    + +
    +
    + + + Last update: + May 28, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/registry/txt/index.html b/v0.13.6/registry/txt/index.html new file mode 100644 index 0000000000..94a43d6fbc --- /dev/null +++ b/v0.13.6/registry/txt/index.html @@ -0,0 +1,2127 @@ + + + + + + + + + + + + + + + + + + TXT - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + +
    +
    +
    + + + +
    +
    +
    + + +
    +
    + + + + + + + + +

    The TXT registry

    +

    The TXT registry is the default registry.
    +It stores DNS record metadata in TXT records, using the same provider.

    +

    Prefixes and Suffixes

    +

    In order to avoid having the registry TXT records collide with
    +TXT or CNAME records created from sources, you can configure a fixed prefix or suffix
    +to be added to the first component of the domain of all registry TXT records.

    +

    The prefix or suffix may not be changed after initial deployment,
    +lest the registry records be orphaned and the metadata be lost.

    +

    The prefix or suffix may contain the substring %{record_type}, which is replaced with
    +the record type of the DNS record for which it is storing metadata.

    +

    The prefix is specified using the --txt-prefix flag and the suffix is specified using
    +the --txt-suffix flag. The two flags are mutually exclusive.

    +

    Wildcard Replacement

    +

    The --txt-wildcard-replacement flag specifies a string to use to replace the “*” in
    +registry TXT records for wildcard domains. Without using this, registry TXT records for
    +wildcard domains will have invalid domain syntax and be rejected by most providers.

    +

    Encryption

    +

    Registry TXT records may contain information, such as the internal ingress name or namespace, considered sensitive, , which attackers could exploit to gather information about your infrastructure.
    +By encrypting TXT records, you can protect this information from unauthorized access.

    +

    Encryption is enabled by using the --txt-encrypt-enabled flag. The 32-byte AES-256-GCM encryption
    +key must be specified in URL-safe base64 form, using the --txt-encrypt-aes-key flag.

    +

    Note that the key used for encryption should be a secure key and properly managed to ensure the security of your TXT records.

    +

    Generating the TXT Encryption Key

    +

    Python
    +

    python -c 'import os,base64; print(base64.urlsafe_b64encode(os.urandom(32)).decode())'
    +

    +

    Bash
    +

    dd if=/dev/urandom bs=32 count=1 2>/dev/null | base64 | tr -d -- '\n' | tr -- '+/' '-_'; echo
    +

    +

    OpenSSL
    +

    openssl rand -base64 32 | tr -- '+/' '-_'
    +

    +

    PowerShell
    +

    # Add System.Web assembly to session, just in case
    +Add-Type -AssemblyName System.Web
    +[Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes([System.Web.Security.Membership]::GeneratePassword(32,4))).Replace("+","-").Replace("/","_")
    +

    +

    Terraform
    +

    resource "random_password" "txt_key" {
    +  length           = 32
    +  override_special = "-_"
    +}
    +

    +

    Manually Encrypting/Decrypting TXT Records

    +

    In some cases you might need to edit registry TXT records. The following example Go code encrypts and decrypts such records.

    +
    package main
    +
    +import (
    +    "fmt"
    +    "sigs.k8s.io/external-dns/endpoint"
    +)
    +
    +func main() {
    +    key := []byte("testtesttesttesttesttesttesttest")
    +    encrypted, _ := endpoint.EncryptText(
    +        "heritage=external-dns,external-dns/owner=example,external-dns/resource=ingress/default/example",
    +        key,
    +        nil,
    +    )
    +    decrypted, _, _ := endpoint.DecryptText(encrypted, key)
    +    fmt.Println(decrypted)
    +}
    +
    +

    Caching

    +

    The TXT registry can optionally cache DNS records read from the provider. This can mitigate
    +rate limits imposed by the provider.

    +

    Caching is enabled by specifying a cache duration with the --txt-cache-interval flag.

    + +
    +
    + + + Last update: + June 2, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/release/index.html b/v0.13.6/release/index.html new file mode 100644 index 0000000000..70dadd8fd9 --- /dev/null +++ b/v0.13.6/release/index.html @@ -0,0 +1,2125 @@ + + + + + + + + + + + + + + + + + + Release - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + +
    +
    +
    + + + +
    +
    +
    + + +
    +
    + + + + + + + + +

    Release

    +

    Release cycle

    +

    Currently we don’t release regularly. Whenever we think it makes sense to release a new version we do it, but we aim to do a new release every month. You might want to ask in our Slack channel external-dns when the next release will come out.

    +

    Versioning convention

    +

    These are the conventions that we will be using for releases following 0.7.6:

    +
      +
    • +

      Patch version should be updated if we need to merge bugfixes, e.g. provider a does need a fix in order make updates working again. I would see updating or improving documentation here.

      +
    • +
    • +

      Minor version should be updated if new features are implemented in existing providers or new provider get introduced.

      +
    • +
    • +

      Major version should be upgraded if we introduce breaking changes.

      +
    • +
    +

    How to release a new image

    +

    Prerequisite

    +

    We use https://github.com/cli/cli to automate the release process. Please install it according to the official documentation.

    +

    You must be an official maintainer of the project to be able to do a release.

    +

    Steps

    +
      +
    • Run scripts/releaser.sh to create a new GitHub release. Alternatively you can create a release in the GitHub UI making sure to click on the autogenerate release node feature.
    • +
    • The step above will trigger the Kubernetes based CI/CD system Prow. Verify that a new image was built and uploaded to gcr.io/k8s-staging-external-dns/external-dns.
    • +
    • Create a PR in the k8s.io repo (see https://github.com/kubernetes/k8s.io/pull/540 for reference) by taking the current staging image using the sha256 digest. Once the PR is merged, the image will be live with the corresponding tag specified in the PR.
    • +
    • Verify that the image is pullable with the given tag (i.e. v0.7.5).
    • +
    • Branch out from the default branch and run scripts/kustomize-version-updater.sh to update the image tag used in the kustomization.yaml.
    • +
    • Create an issue to release the corresponding Helm chart via the chart release process (below) assigned to a chart maintainer
    • +
    • Create a PR with the kustomize change.
    • +
    • Create a PR to replace all versions for docker images in the tutorials. A possible script to use is sd registry.k8s.io/external-dns/external-dns:.* registry.k8s.io/external-dns/external-dns:v0.13.2 $(fd --type file) which uses the fd and sd utilities.
    • +
    • Once the PR is merged, all is done :-)
    • +
    +

    How to release a new chart version

    +

    The chart needs to be released in response to an ExternalDNS image release or on an as-needed basis; this should be triggered by an issue to release the chart.

    +

    Steps

    +
      +
    • Create a PR to update Chart.yaml with the ExternalDNS version in appVersion, agreed on chart release version in version and annotations showing the changes
    • +
    • Validate that the chart linting is successful
    • +
    • Merge the PR to trigger a GitHub action to release the chart
    • +
    + +
    +
    + + + Last update: + February 4, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/scripts/copy_docs.sh b/v0.13.6/scripts/copy_docs.sh new file mode 100644 index 0000000000..7e114b84af --- /dev/null +++ b/v0.13.6/scripts/copy_docs.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +set -eo pipefail + +cp CONTRIBUTING.md code-of-conduct.md ./docs/ + +cp LICENSE ./docs/LICENSE.md + +cp README.md ./docs/index.md diff --git a/v0.13.6/scripts/docs.go b/v0.13.6/scripts/docs.go new file mode 100644 index 0000000000..59fdbd43f8 --- /dev/null +++ b/v0.13.6/scripts/docs.go @@ -0,0 +1,48 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "log" + "os" + "strings" +) + +func removeLinkPrefixInIndex() { + content, err := os.ReadFile("./docs/index.md") + if err != nil { + log.Fatalf("Could not read index.md file. Make sure to run copy_docs.sh script first. Original error: %s", err) + } + + updatedContent := strings.ReplaceAll(string(content), "](./docs/", "](") + updatedContent = strings.ReplaceAll(updatedContent, "](docs/", "](") + updatedContent = strings.ReplaceAll(updatedContent, "docs/img/external-dns.png", "img/external-dns.png") + + f, err := os.OpenFile("./docs/index.md", os.O_RDWR, 0o644) + if err != nil { + log.Fatalf("Could not open index.md file to update content. Original error: %s", err) + } + defer f.Close() + + if _, err := f.WriteString(updatedContent); err != nil { + log.Fatalf("Failed writing links update to index.md. Original error: %s", err) + } +} + +func main() { + removeLinkPrefixInIndex() +} diff --git a/v0.13.6/scripts/index.html.gotmpl b/v0.13.6/scripts/index.html.gotmpl new file mode 100644 index 0000000000..26b836ebac --- /dev/null +++ b/v0.13.6/scripts/index.html.gotmpl @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/v0.13.6/scripts/requirements.txt b/v0.13.6/scripts/requirements.txt new file mode 100644 index 0000000000..cacaecf196 --- /dev/null +++ b/v0.13.6/scripts/requirements.txt @@ -0,0 +1,5 @@ +mkdocs-git-revision-date-localized-plugin == 1.0.0 +mkdocs == 1.3.0 +mkdocs-material == 8.2.8 +mkdocs-literate-nav == 0.4.0 +mike == 1.1.2 \ No newline at end of file diff --git a/v0.13.6/search/search_index.json b/v0.13.6/search/search_index.json new file mode 100644 index 0000000000..239ce7c45f --- /dev/null +++ b/v0.13.6/search/search_index.json @@ -0,0 +1 @@ +{"config":{"indexing":"full","lang":["en"],"min_search_length":3,"prebuild_index":false,"separator":"[\\s\\-]+"},"docs":[{"location":"","text":"ExternalDNS \u00b6 ExternalDNS synchronizes exposed Kubernetes Services and Ingresses with DNS providers. What It Does \u00b6 Inspired by Kubernetes DNS , Kubernetes\u2019 cluster-internal DNS server, ExternalDNS makes Kubernetes resources discoverable via public DNS servers. Like KubeDNS, it retrieves a list of resources (Services, Ingresses, etc.) from the Kubernetes API to determine a desired list of DNS records. Unlike KubeDNS, however, it\u2019s not a DNS server itself, but merely configures other DNS providers accordingly\u2014e.g. AWS Route 53 or Google Cloud DNS . In a broader sense, ExternalDNS allows you to control DNS records dynamically via Kubernetes resources in a DNS provider-agnostic way. The FAQ contains additional information and addresses several questions about key concepts of ExternalDNS. To see ExternalDNS in action, have a look at this video or read this blogpost . The Latest Release \u00b6 ExternalDNS allows you to keep selected zones (via --domain-filter ) synchronized with Ingresses and Services of type=LoadBalancer and nodes in various DNS providers: * Google Cloud DNS * AWS Route 53 * AWS Cloud Map * AzureDNS * BlueCat * Civo * CloudFlare * RcodeZero * DigitalOcean * DNSimple * Infoblox * Dyn * OpenStack Designate * PowerDNS * CoreDNS * Exoscale * Oracle Cloud Infrastructure DNS * Linode DNS * RFC2136 * NS1 * TransIP * VinylDNS * Vultr * OVH * Scaleway * Akamai Edge DNS * GoDaddy * Gandi * ANS Group SafeDNS * IBM Cloud DNS * TencentCloud PrivateDNS * TencentCloud DNSPod * Plural * Pi-hole ExternalDNS is, by default, aware of the records it is managing, therefore it can safely manage non-empty hosted zones. We strongly encourage you to set --txt-owner-id to a unique value that doesn\u2019t change for the lifetime of your cluster. You might also want to run ExternalDNS in a dry run mode ( --dry-run flag) to see the changes to be submitted to your DNS Provider API. Note that all flags can be replaced with environment variables; for instance, --dry-run could be replaced with EXTERNAL_DNS_DRY_RUN=1 . Status of providers \u00b6 ExternalDNS supports multiple DNS providers which have been implemented by the ExternalDNS contributors . Maintaining all of those in a central repository is a challenge and we have limited resources to test changes. This means that it is very hard to test all providers for possible regressions and, as written in the Contributing section, we encourage contributors to step in as maintainers for the individual providers and help by testing the integrations. End-to-end testing of ExternalDNS is currently performed in the separate kubernetes-on-aws repository. We define the following stability levels for providers: Stable : Used for smoke tests before a release, used in production and maintainers are active. Beta : Community supported, well tested, but maintainers have no access to resources to execute integration tests on the real platform and/or are not using it in production. Alpha : Community provided with no support from the maintainers apart from reviewing PRs. The following table clarifies the current status of the providers according to the aforementioned stability levels: Provider Status Maintainers Google Cloud DNS Stable AWS Route 53 Stable AWS Cloud Map Beta Akamai Edge DNS Beta AzureDNS Beta BlueCat Alpha @seanmalloy @vinny-sabatini Civo Alpha @alejandrojnm CloudFlare Beta RcodeZero Alpha DigitalOcean Alpha DNSimple Alpha Infoblox Alpha @saileshgiri Dyn Alpha OpenStack Designate Alpha PowerDNS Alpha CoreDNS Alpha Exoscale Alpha Oracle Cloud Infrastructure DNS Alpha Linode DNS Alpha RFC2136 Alpha NS1 Alpha TransIP Alpha VinylDNS Alpha RancherDNS Alpha OVH Alpha Scaleway DNS Alpha @Sh4d1 Vultr Alpha UltraDNS Alpha GoDaddy Alpha Gandi Alpha @packi SafeDNS Alpha @assureddt IBMCloud Alpha @hughhuangzh TencentCloud Alpha @Hyzhou Plural Alpha @michaeljguarino Pi-hole Alpha @tinyzimmer Kubernetes version compatibility \u00b6 A breaking change was added in external-dns v0.10.0. ExternalDNS <= 0.9.x >= 0.10.0 Kubernetes <= 1.18 Kubernetes >= 1.19 and <= 1.21 Kubernetes >= 1.22 Running ExternalDNS: \u00b6 The are two ways of running ExternalDNS: Deploying to a Cluster Running Locally Deploying to a Cluster \u00b6 The following tutorials are provided: Akamai Edge DNS Alibaba Cloud AWS AWS Load Balancer Controller Route53 Same domain for public and private Route53 zones Cloud Map Kube Ingress AWS Controller Azure DNS Azure Private DNS Civo Cloudflare BlueCat CoreDNS DigitalOcean DNSimple Dyn Exoscale ExternalName Services Google Kubernetes Engine Using Google\u2019s Default Ingress Controller Using the Nginx Ingress Controller Headless Services Infoblox Istio Gateway Source Kubernetes Security Context Linode Nginx Ingress Controller NS1 NS Record Creation with CRD Source MX Record Creation with CRD Source OpenStack Designate Oracle Cloud Infrastructure (OCI) DNS PowerDNS RcodeZero RancherDNS (RDNS) RFC2136 TransIP VinylDNS OVH Scaleway Vultr UltraDNS GoDaddy Gandi SafeDNS IBM Cloud Nodes as source TencentCloud Plural Pi-hole Running Locally \u00b6 See the contributor guide for details on compiling from source. Setup Steps \u00b6 Next, run an application and expose it via a Kubernetes Service: kubectl run nginx --image=nginx --port=80 kubectl expose pod nginx --port=80 --target-port=80 --type=LoadBalancer Annotate the Service with your desired external DNS name. Make sure to change example.org to your domain. kubectl annotate service nginx \"external-dns.alpha.kubernetes.io/hostname=nginx.example.org.\" Optionally, you can customize the TTL value of the resulting DNS record by using the external-dns.alpha.kubernetes.io/ttl annotation: kubectl annotate service nginx \"external-dns.alpha.kubernetes.io/ttl=10\" For more details on configuring TTL, see here . Use the internal-hostname annotation to create DNS records with ClusterIP as the target. kubectl annotate service nginx \"external-dns.alpha.kubernetes.io/internal-hostname=nginx.internal.example.org.\" If the service is not of type Loadbalancer you need the \u2013publish-internal-services flag. Locally run a single sync loop of ExternalDNS. external-dns --txt-owner-id my-cluster-id --provider google --google-project example-project --source service --once --dry-run This should output the DNS records it will modify to match the managed zone with the DNS records you desire. It also assumes you are running in the default namespace. See the FAQ for more information regarding namespaces. Note: TXT records will have the my-cluster-id value embedded. Those are used to ensure that ExternalDNS is aware of the records it manages. Once you\u2019re satisfied with the result, you can run ExternalDNS like you would run it in your cluster: as a control loop, and not in dry-run mode: external-dns --txt-owner-id my-cluster-id --provider google --google-project example-project --source service Check that ExternalDNS has created the desired DNS record for your Service and that it points to its load balancer\u2019s IP. Then try to resolve it: dig +short nginx.example.org. 104.155.60.49 Now you can experiment and watch how ExternalDNS makes sure that your DNS records are configured as desired. Here are a couple of things you can try out: * Change the desired hostname by modifying the Service\u2019s annotation. * Recreate the Service and see that the DNS record will be updated to point to the new load balancer IP. * Add another Service to create more DNS records. * Remove Services to clean up your managed zone. The tutorials section contains examples, including Ingress resources, and shows you how to set up ExternalDNS in different environments such as other cloud providers and alternative Ingress controllers. Note \u00b6 If using a txt registry and attempting to use a CNAME the --txt-prefix must be set to avoid conflicts. Changing --txt-prefix will result in lost ownership over previously created records. If externalIPs list is defined for a LoadBalancer service, this list will be used instead of an assigned load balancer IP to create a DNS record. It\u2019s useful when you run bare metal Kubernetes clusters behind NAT or in a similar setup, where a load balancer IP differs from a public IP (e.g. with MetalLB ). Contributing \u00b6 Are you interested in contributing to external-dns? We, the maintainers and community, would love your suggestions, contributions, and help! Also, the maintainers can be contacted at any time to learn more about how to get involved. We also encourage ALL active community participants to act as if they are maintainers, even if you don\u2019t have \u201cofficial\u201d write permissions. This is a community effort, we are here to serve the Kubernetes community. If you have an active interest and you want to get involved, you have real power! Don\u2019t assume that the only people who can get things done around here are the \u201cmaintainers\u201d. We also would love to add more \u201cofficial\u201d maintainers, so show us what you can do! The external-dns project is currently in need of maintainers for specific DNS providers. Ideally each provider would have at least two maintainers. It would be nice if the maintainers run the provider in production, but it is not strictly required. Provider listed here that do not have a maintainer listed are in need of assistance. Read the contributing guidelines and have a look at the contributing docs to learn about building the project, the project structure, and the purpose of each package. For an overview on how to write new Sources and Providers check out Sources and Providers . Heritage \u00b6 ExternalDNS is an effort to unify the following similar projects in order to bring the Kubernetes community an easy and predictable way of managing DNS records across cloud providers based on their Kubernetes resources: Kops\u2019 DNS Controller Zalando\u2019s Mate Molecule Software\u2019s route53-kubernetes User Demo How-To Blogs and Examples \u00b6 A full demo on GKE Kubernetes. See How-to Kubernetes with DNS management (ssl-manager pre-req) Run external-dns on GKE with workload identity. See Kubernetes, ingress-nginx, cert-manager & external-dns ](https://medium.com/@jpantjsoha/how-to-kubernetes-with-dns-management-for-gitops-31239ea75d8d) Run external-dns on GKE with workload identity. See Kubernetes, ingress-nginx, cert-manager & external-dns","title":"Home"},{"location":"#externaldns","text":"ExternalDNS synchronizes exposed Kubernetes Services and Ingresses with DNS providers.","title":"ExternalDNS"},{"location":"#what-it-does","text":"Inspired by Kubernetes DNS , Kubernetes\u2019 cluster-internal DNS server, ExternalDNS makes Kubernetes resources discoverable via public DNS servers. Like KubeDNS, it retrieves a list of resources (Services, Ingresses, etc.) from the Kubernetes API to determine a desired list of DNS records. Unlike KubeDNS, however, it\u2019s not a DNS server itself, but merely configures other DNS providers accordingly\u2014e.g. AWS Route 53 or Google Cloud DNS . In a broader sense, ExternalDNS allows you to control DNS records dynamically via Kubernetes resources in a DNS provider-agnostic way. The FAQ contains additional information and addresses several questions about key concepts of ExternalDNS. To see ExternalDNS in action, have a look at this video or read this blogpost .","title":"What It Does"},{"location":"#the-latest-release","text":"ExternalDNS allows you to keep selected zones (via --domain-filter ) synchronized with Ingresses and Services of type=LoadBalancer and nodes in various DNS providers: * Google Cloud DNS * AWS Route 53 * AWS Cloud Map * AzureDNS * BlueCat * Civo * CloudFlare * RcodeZero * DigitalOcean * DNSimple * Infoblox * Dyn * OpenStack Designate * PowerDNS * CoreDNS * Exoscale * Oracle Cloud Infrastructure DNS * Linode DNS * RFC2136 * NS1 * TransIP * VinylDNS * Vultr * OVH * Scaleway * Akamai Edge DNS * GoDaddy * Gandi * ANS Group SafeDNS * IBM Cloud DNS * TencentCloud PrivateDNS * TencentCloud DNSPod * Plural * Pi-hole ExternalDNS is, by default, aware of the records it is managing, therefore it can safely manage non-empty hosted zones. We strongly encourage you to set --txt-owner-id to a unique value that doesn\u2019t change for the lifetime of your cluster. You might also want to run ExternalDNS in a dry run mode ( --dry-run flag) to see the changes to be submitted to your DNS Provider API. Note that all flags can be replaced with environment variables; for instance, --dry-run could be replaced with EXTERNAL_DNS_DRY_RUN=1 .","title":"The Latest Release"},{"location":"#status-of-providers","text":"ExternalDNS supports multiple DNS providers which have been implemented by the ExternalDNS contributors . Maintaining all of those in a central repository is a challenge and we have limited resources to test changes. This means that it is very hard to test all providers for possible regressions and, as written in the Contributing section, we encourage contributors to step in as maintainers for the individual providers and help by testing the integrations. End-to-end testing of ExternalDNS is currently performed in the separate kubernetes-on-aws repository. We define the following stability levels for providers: Stable : Used for smoke tests before a release, used in production and maintainers are active. Beta : Community supported, well tested, but maintainers have no access to resources to execute integration tests on the real platform and/or are not using it in production. Alpha : Community provided with no support from the maintainers apart from reviewing PRs. The following table clarifies the current status of the providers according to the aforementioned stability levels: Provider Status Maintainers Google Cloud DNS Stable AWS Route 53 Stable AWS Cloud Map Beta Akamai Edge DNS Beta AzureDNS Beta BlueCat Alpha @seanmalloy @vinny-sabatini Civo Alpha @alejandrojnm CloudFlare Beta RcodeZero Alpha DigitalOcean Alpha DNSimple Alpha Infoblox Alpha @saileshgiri Dyn Alpha OpenStack Designate Alpha PowerDNS Alpha CoreDNS Alpha Exoscale Alpha Oracle Cloud Infrastructure DNS Alpha Linode DNS Alpha RFC2136 Alpha NS1 Alpha TransIP Alpha VinylDNS Alpha RancherDNS Alpha OVH Alpha Scaleway DNS Alpha @Sh4d1 Vultr Alpha UltraDNS Alpha GoDaddy Alpha Gandi Alpha @packi SafeDNS Alpha @assureddt IBMCloud Alpha @hughhuangzh TencentCloud Alpha @Hyzhou Plural Alpha @michaeljguarino Pi-hole Alpha @tinyzimmer","title":"Status of providers"},{"location":"#kubernetes-version-compatibility","text":"A breaking change was added in external-dns v0.10.0. ExternalDNS <= 0.9.x >= 0.10.0 Kubernetes <= 1.18 Kubernetes >= 1.19 and <= 1.21 Kubernetes >= 1.22","title":"Kubernetes version compatibility"},{"location":"#running-externaldns","text":"The are two ways of running ExternalDNS: Deploying to a Cluster Running Locally","title":"Running ExternalDNS:"},{"location":"#deploying-to-a-cluster","text":"The following tutorials are provided: Akamai Edge DNS Alibaba Cloud AWS AWS Load Balancer Controller Route53 Same domain for public and private Route53 zones Cloud Map Kube Ingress AWS Controller Azure DNS Azure Private DNS Civo Cloudflare BlueCat CoreDNS DigitalOcean DNSimple Dyn Exoscale ExternalName Services Google Kubernetes Engine Using Google\u2019s Default Ingress Controller Using the Nginx Ingress Controller Headless Services Infoblox Istio Gateway Source Kubernetes Security Context Linode Nginx Ingress Controller NS1 NS Record Creation with CRD Source MX Record Creation with CRD Source OpenStack Designate Oracle Cloud Infrastructure (OCI) DNS PowerDNS RcodeZero RancherDNS (RDNS) RFC2136 TransIP VinylDNS OVH Scaleway Vultr UltraDNS GoDaddy Gandi SafeDNS IBM Cloud Nodes as source TencentCloud Plural Pi-hole","title":"Deploying to a Cluster"},{"location":"#running-locally","text":"See the contributor guide for details on compiling from source.","title":"Running Locally"},{"location":"#setup-steps","text":"Next, run an application and expose it via a Kubernetes Service: kubectl run nginx --image=nginx --port=80 kubectl expose pod nginx --port=80 --target-port=80 --type=LoadBalancer Annotate the Service with your desired external DNS name. Make sure to change example.org to your domain. kubectl annotate service nginx \"external-dns.alpha.kubernetes.io/hostname=nginx.example.org.\" Optionally, you can customize the TTL value of the resulting DNS record by using the external-dns.alpha.kubernetes.io/ttl annotation: kubectl annotate service nginx \"external-dns.alpha.kubernetes.io/ttl=10\" For more details on configuring TTL, see here . Use the internal-hostname annotation to create DNS records with ClusterIP as the target. kubectl annotate service nginx \"external-dns.alpha.kubernetes.io/internal-hostname=nginx.internal.example.org.\" If the service is not of type Loadbalancer you need the \u2013publish-internal-services flag. Locally run a single sync loop of ExternalDNS. external-dns --txt-owner-id my-cluster-id --provider google --google-project example-project --source service --once --dry-run This should output the DNS records it will modify to match the managed zone with the DNS records you desire. It also assumes you are running in the default namespace. See the FAQ for more information regarding namespaces. Note: TXT records will have the my-cluster-id value embedded. Those are used to ensure that ExternalDNS is aware of the records it manages. Once you\u2019re satisfied with the result, you can run ExternalDNS like you would run it in your cluster: as a control loop, and not in dry-run mode: external-dns --txt-owner-id my-cluster-id --provider google --google-project example-project --source service Check that ExternalDNS has created the desired DNS record for your Service and that it points to its load balancer\u2019s IP. Then try to resolve it: dig +short nginx.example.org. 104.155.60.49 Now you can experiment and watch how ExternalDNS makes sure that your DNS records are configured as desired. Here are a couple of things you can try out: * Change the desired hostname by modifying the Service\u2019s annotation. * Recreate the Service and see that the DNS record will be updated to point to the new load balancer IP. * Add another Service to create more DNS records. * Remove Services to clean up your managed zone. The tutorials section contains examples, including Ingress resources, and shows you how to set up ExternalDNS in different environments such as other cloud providers and alternative Ingress controllers.","title":"Setup Steps"},{"location":"#note","text":"If using a txt registry and attempting to use a CNAME the --txt-prefix must be set to avoid conflicts. Changing --txt-prefix will result in lost ownership over previously created records. If externalIPs list is defined for a LoadBalancer service, this list will be used instead of an assigned load balancer IP to create a DNS record. It\u2019s useful when you run bare metal Kubernetes clusters behind NAT or in a similar setup, where a load balancer IP differs from a public IP (e.g. with MetalLB ).","title":"Note"},{"location":"#contributing","text":"Are you interested in contributing to external-dns? We, the maintainers and community, would love your suggestions, contributions, and help! Also, the maintainers can be contacted at any time to learn more about how to get involved. We also encourage ALL active community participants to act as if they are maintainers, even if you don\u2019t have \u201cofficial\u201d write permissions. This is a community effort, we are here to serve the Kubernetes community. If you have an active interest and you want to get involved, you have real power! Don\u2019t assume that the only people who can get things done around here are the \u201cmaintainers\u201d. We also would love to add more \u201cofficial\u201d maintainers, so show us what you can do! The external-dns project is currently in need of maintainers for specific DNS providers. Ideally each provider would have at least two maintainers. It would be nice if the maintainers run the provider in production, but it is not strictly required. Provider listed here that do not have a maintainer listed are in need of assistance. Read the contributing guidelines and have a look at the contributing docs to learn about building the project, the project structure, and the purpose of each package. For an overview on how to write new Sources and Providers check out Sources and Providers .","title":"Contributing"},{"location":"#heritage","text":"ExternalDNS is an effort to unify the following similar projects in order to bring the Kubernetes community an easy and predictable way of managing DNS records across cloud providers based on their Kubernetes resources: Kops\u2019 DNS Controller Zalando\u2019s Mate Molecule Software\u2019s route53-kubernetes","title":"Heritage"},{"location":"#user-demo-how-to-blogs-and-examples","text":"A full demo on GKE Kubernetes. See How-to Kubernetes with DNS management (ssl-manager pre-req) Run external-dns on GKE with workload identity. See Kubernetes, ingress-nginx, cert-manager & external-dns ](https://medium.com/@jpantjsoha/how-to-kubernetes-with-dns-management-for-gitops-31239ea75d8d) Run external-dns on GKE with workload identity. See Kubernetes, ingress-nginx, cert-manager & external-dns","title":"User Demo How-To Blogs and Examples"},{"location":"20190708-external-dns-incubator/","text":"Move ExternalDNS out of Kubernetes incubator \u00b6 Move ExternalDNS out of Kubernetes incubator Summary Motivation Goals Proposal Details Graduation Criteria Maintainers Release process, artifacts Risks and Mitigations Summary \u00b6 ExternalDNS is a project that synchronizes Kubernetes\u2019 Services, Ingresses and other Kubernetes resources to DNS backends for several DNS providers. The projects was started as a Kubernetes Incubator project in February 2017 and being the Kubernetes incubation initiative officially over, the maintainers want to propose the project to be moved to the kubernetes GitHub organization or to kubernetes-sigs, under the sponsorship of sig-network. Motivation \u00b6 ExternalDNS started as a community project with the goal of unifying several existing projects that were trying to solve the same problem: create DNS records for Kubernetes resources on several DNS backends. When the project was proposed (see the original discussion ), there were at least 3 existing implementations of the same functionality: Mate - https://github.com/linki/mate DNS-controller from kops - https://github.com/kubernetes/kops/tree/HEAD/dns-controller Route53-kubernetes - https://github.com/wearemolecule/route53-kubernetes ExternalDNS\u2019 goal from the beginning was to provide an officially supported solution to those problems. After two years of development, the project is still in the kubernetes-sigs. The incubation has been officially discontinued and to quote @thockin \u201cIncubator projects should either become real projects in Kubernetes, shut themselves down, or move elsewhere\u201d (see original thread here ). This KEP proposes to move ExternalDNS to the main Kubernetes organization or kubernetes-sigs. The \u201cProposal\u201d section details the reasons behind it. Goals \u00b6 The only goal of this KEP is to establish consensus regarding the future of the ExternalDNS project and determine where it belongs. Proposal \u00b6 This KEP is about moving External DNS out of the Kubernetes incubator. This section will cover the reasons why External DNS is useful and what the community would miss in case the project would be discontinued or moved under another organization. External DNS\u2026 Is the de facto solution to create DNS records for several Kubernetes resources. Is a vital component to achieve an experience close to a PaaS that many Kubernetes users try to replicate on top of Kubernetes, by allowing to automatically create DNS records for web applications. Supports already 18 different DNS providers including all major public clouds (AWS, Azure, GCP). Given that the kubernetes-sigs organization will eventually be shut down, the possible alternatives to moving to be an official Kubernetes project are the following: Shut down the project Move the project elsewhere We believe that those alternatives would result in a worse outcome for the community compared to moving the project to the any of the other official Kubernetes organizations. In fact, shutting down ExternalDNS can cause: The community to rebuild the same solution as already happened multiple times before the project was launched. Currently ExternalDNS is easy to be found, referenced in many articles/tutorials and for that reason not exposed to that risk. Existing users of the projects to be left without a future proof working solution. Moving the ExternalDNS project outside of Kubernetes projects would cause: Problems (re-)establishing user trust which could eventually lead to fragmentation and duplication. It would be hard to establish in which organization the project should be moved to. The most natural would be Zalando\u2019s organization, being the company that put most of the work on the project. While it is possible to assume Zalando\u2019s commitment to open-source, that would be a strategic mistake for the project community and for the Kubernetes ecosystem due to the obvious lack of neutrality. Lack of resources to test, lack of issue management via automation. For those reasons, we propose to move ExternalDNS out of the Kubernetes incubator, to live either under the kubernetes or kubernetes-sigs organization to keep being a vital part of the Kubernetes ecosystem. Details \u00b6 Graduation Criteria \u00b6 ExternalDNS is a two years old project widely used in production by many companies. The implementation for the three major cloud providers (AWS, Azure, GCP) is stable, not changing its logic and the project is being used in production by many company using Kubernetes. We have evidence that many companies are using ExternalDNS in production, but it is out of scope for this proposal to collect a comprehensive list of companies. The project was quoted by a number of tutorials on the web, including the official tutorials from AWS . ExternalDNS can\u2019t be consider to be \u201cdone\u201d: while the core functionality has been implemented, there is lack of integration testing and structural changes that are needed. Those are identified in the project roadmap, which is roughly made of the following items: Decoupling of the providers Implementation proposal Development Bug fixing and performance optimization (i.e. rate limiting on cloud providers) Integration testing suite, to be implemented at least for the \u201cstable\u201d providers For those reasons, we consider ExternalDNS to be in Beta state as a project. We believe that once the items mentioned above will be implemented, the project can reach a declared GA status. There are a number of other factors that need to be covered to fully describe the state of the project, including who are the maintainers, the way we release and manage the project and so on. Maintainers \u00b6 The project has the following maintainers: hjacobs Raffo linki njuettner The list of maintainers shrunk over time as people moved out of the original development team (all the team members were working at Zalando at the time of project creation) and the project required less work. The high number of providers contributed to the project pose a maintainability challenge: it is hard to bring the providers forward in terms of functionalities or even test them. The maintainers believe that the plan to transform the current Provider interface from a Go interface to an API will allow for enough decoupling and to hand over the maintenance of those plugins to the contributors themselves, see the risk and mitigations section for further details. Release process, artifacts \u00b6 The project uses the free quota of TravisCI to run tests for the project. The release pipeline for the project is currently fully owned by Zalando. It runs on the internal system of the company (closed source) which external maintainers/users can\u2019t access and that pushes images to the publicly accessible docker registry available at the URL registry.opensource.zalan.do . The docker registry service is provided as best effort with no sort of SLA and the maintainers team openly suggests the users to build and maintain their own docker image based on the provided Dockerfiles. Providing a vanity URL for the docker images was consider a non goal till now, but the community seems to be wanting official images from a GCR domain, similarly to what is available for other parts of official Kubernetes projects. ExternalDNS does not follow a specific release cycle. Releases are made often when there are major contributions (i.e. new providers) or important bug fixes. That said, the default branch is considered stable and can be used as well to build images. Risks and Mitigations \u00b6 The following are risks that were identified: Low number of maintainers: we are currently facing issues keeping up with the number of pull requests and issues giving the low number of maintainers. The list of maintainers already shrunk from 8 maintainers to 4. Issues maintaining community contributed providers: we often lack access to external providers (i.e. InfoBlox, etc.) and this means that we cannot verify the implementations and/or run regression tests that go beyond unit testing. Somewhat low quality of releases due to lack of integration testing. We think that the following actions will constitute appropriate mitigations: Decoupling the providers via an API will allow us to resolve the problem of the providers. Being the project already more than 2 years old and given that there are 18 providers implemented, we possess enough information to define an API that we can be stable in a short timeframe. Once this is stable, the problem of testing the providers can be deferred to be a provider\u2019s responsibility. This will also reduce the scope of External DNS core code, which means that there will be no need for a further increase of the maintaining team. We added integration testing for the main cloud providers to the roadmap for the 1.0 release to make sure that we cover the mostly used ones. We believe that this item should be tackled independently from the decoupling of providers as it would be capable of generating value independently from the result of the decoupling efforts. With the move to the Kubernetes incubation, we hope that we will be able to access the testing resources of the Kubernetes project. In this way, we hope to decouple the project from the dependency on Zalando\u2019s internal CI tool. This will help open up the possibility to increase the visibility on the project from external contributors, which currently would be blocked by the lack of access to the software used for the whole release pipeline.","title":"Out of Incubator"},{"location":"20190708-external-dns-incubator/#move-externaldns-out-of-kubernetes-incubator","text":"Move ExternalDNS out of Kubernetes incubator Summary Motivation Goals Proposal Details Graduation Criteria Maintainers Release process, artifacts Risks and Mitigations","title":"Move ExternalDNS out of Kubernetes incubator"},{"location":"20190708-external-dns-incubator/#summary","text":"ExternalDNS is a project that synchronizes Kubernetes\u2019 Services, Ingresses and other Kubernetes resources to DNS backends for several DNS providers. The projects was started as a Kubernetes Incubator project in February 2017 and being the Kubernetes incubation initiative officially over, the maintainers want to propose the project to be moved to the kubernetes GitHub organization or to kubernetes-sigs, under the sponsorship of sig-network.","title":"Summary"},{"location":"20190708-external-dns-incubator/#motivation","text":"ExternalDNS started as a community project with the goal of unifying several existing projects that were trying to solve the same problem: create DNS records for Kubernetes resources on several DNS backends. When the project was proposed (see the original discussion ), there were at least 3 existing implementations of the same functionality: Mate - https://github.com/linki/mate DNS-controller from kops - https://github.com/kubernetes/kops/tree/HEAD/dns-controller Route53-kubernetes - https://github.com/wearemolecule/route53-kubernetes ExternalDNS\u2019 goal from the beginning was to provide an officially supported solution to those problems. After two years of development, the project is still in the kubernetes-sigs. The incubation has been officially discontinued and to quote @thockin \u201cIncubator projects should either become real projects in Kubernetes, shut themselves down, or move elsewhere\u201d (see original thread here ). This KEP proposes to move ExternalDNS to the main Kubernetes organization or kubernetes-sigs. The \u201cProposal\u201d section details the reasons behind it.","title":"Motivation"},{"location":"20190708-external-dns-incubator/#goals","text":"The only goal of this KEP is to establish consensus regarding the future of the ExternalDNS project and determine where it belongs.","title":"Goals"},{"location":"20190708-external-dns-incubator/#proposal","text":"This KEP is about moving External DNS out of the Kubernetes incubator. This section will cover the reasons why External DNS is useful and what the community would miss in case the project would be discontinued or moved under another organization. External DNS\u2026 Is the de facto solution to create DNS records for several Kubernetes resources. Is a vital component to achieve an experience close to a PaaS that many Kubernetes users try to replicate on top of Kubernetes, by allowing to automatically create DNS records for web applications. Supports already 18 different DNS providers including all major public clouds (AWS, Azure, GCP). Given that the kubernetes-sigs organization will eventually be shut down, the possible alternatives to moving to be an official Kubernetes project are the following: Shut down the project Move the project elsewhere We believe that those alternatives would result in a worse outcome for the community compared to moving the project to the any of the other official Kubernetes organizations. In fact, shutting down ExternalDNS can cause: The community to rebuild the same solution as already happened multiple times before the project was launched. Currently ExternalDNS is easy to be found, referenced in many articles/tutorials and for that reason not exposed to that risk. Existing users of the projects to be left without a future proof working solution. Moving the ExternalDNS project outside of Kubernetes projects would cause: Problems (re-)establishing user trust which could eventually lead to fragmentation and duplication. It would be hard to establish in which organization the project should be moved to. The most natural would be Zalando\u2019s organization, being the company that put most of the work on the project. While it is possible to assume Zalando\u2019s commitment to open-source, that would be a strategic mistake for the project community and for the Kubernetes ecosystem due to the obvious lack of neutrality. Lack of resources to test, lack of issue management via automation. For those reasons, we propose to move ExternalDNS out of the Kubernetes incubator, to live either under the kubernetes or kubernetes-sigs organization to keep being a vital part of the Kubernetes ecosystem.","title":"Proposal"},{"location":"20190708-external-dns-incubator/#details","text":"","title":"Details"},{"location":"20190708-external-dns-incubator/#graduation-criteria","text":"ExternalDNS is a two years old project widely used in production by many companies. The implementation for the three major cloud providers (AWS, Azure, GCP) is stable, not changing its logic and the project is being used in production by many company using Kubernetes. We have evidence that many companies are using ExternalDNS in production, but it is out of scope for this proposal to collect a comprehensive list of companies. The project was quoted by a number of tutorials on the web, including the official tutorials from AWS . ExternalDNS can\u2019t be consider to be \u201cdone\u201d: while the core functionality has been implemented, there is lack of integration testing and structural changes that are needed. Those are identified in the project roadmap, which is roughly made of the following items: Decoupling of the providers Implementation proposal Development Bug fixing and performance optimization (i.e. rate limiting on cloud providers) Integration testing suite, to be implemented at least for the \u201cstable\u201d providers For those reasons, we consider ExternalDNS to be in Beta state as a project. We believe that once the items mentioned above will be implemented, the project can reach a declared GA status. There are a number of other factors that need to be covered to fully describe the state of the project, including who are the maintainers, the way we release and manage the project and so on.","title":"Graduation Criteria"},{"location":"20190708-external-dns-incubator/#maintainers","text":"The project has the following maintainers: hjacobs Raffo linki njuettner The list of maintainers shrunk over time as people moved out of the original development team (all the team members were working at Zalando at the time of project creation) and the project required less work. The high number of providers contributed to the project pose a maintainability challenge: it is hard to bring the providers forward in terms of functionalities or even test them. The maintainers believe that the plan to transform the current Provider interface from a Go interface to an API will allow for enough decoupling and to hand over the maintenance of those plugins to the contributors themselves, see the risk and mitigations section for further details.","title":"Maintainers"},{"location":"20190708-external-dns-incubator/#release-process-artifacts","text":"The project uses the free quota of TravisCI to run tests for the project. The release pipeline for the project is currently fully owned by Zalando. It runs on the internal system of the company (closed source) which external maintainers/users can\u2019t access and that pushes images to the publicly accessible docker registry available at the URL registry.opensource.zalan.do . The docker registry service is provided as best effort with no sort of SLA and the maintainers team openly suggests the users to build and maintain their own docker image based on the provided Dockerfiles. Providing a vanity URL for the docker images was consider a non goal till now, but the community seems to be wanting official images from a GCR domain, similarly to what is available for other parts of official Kubernetes projects. ExternalDNS does not follow a specific release cycle. Releases are made often when there are major contributions (i.e. new providers) or important bug fixes. That said, the default branch is considered stable and can be used as well to build images.","title":"Release process, artifacts"},{"location":"20190708-external-dns-incubator/#risks-and-mitigations","text":"The following are risks that were identified: Low number of maintainers: we are currently facing issues keeping up with the number of pull requests and issues giving the low number of maintainers. The list of maintainers already shrunk from 8 maintainers to 4. Issues maintaining community contributed providers: we often lack access to external providers (i.e. InfoBlox, etc.) and this means that we cannot verify the implementations and/or run regression tests that go beyond unit testing. Somewhat low quality of releases due to lack of integration testing. We think that the following actions will constitute appropriate mitigations: Decoupling the providers via an API will allow us to resolve the problem of the providers. Being the project already more than 2 years old and given that there are 18 providers implemented, we possess enough information to define an API that we can be stable in a short timeframe. Once this is stable, the problem of testing the providers can be deferred to be a provider\u2019s responsibility. This will also reduce the scope of External DNS core code, which means that there will be no need for a further increase of the maintaining team. We added integration testing for the main cloud providers to the roadmap for the 1.0 release to make sure that we cover the mostly used ones. We believe that this item should be tackled independently from the decoupling of providers as it would be capable of generating value independently from the result of the decoupling efforts. With the move to the Kubernetes incubation, we hope that we will be able to access the testing resources of the Kubernetes project. In this way, we hope to decouple the project from the dependency on Zalando\u2019s internal CI tool. This will help open up the possibility to increase the visibility on the project from external contributors, which currently would be blocked by the lack of access to the software used for the whole release pipeline.","title":"Risks and Mitigations"},{"location":"CONTRIBUTING/","text":"Contributing Guidelines \u00b6 Welcome to Kubernetes. We are excited about the prospect of you joining our community ! The Kubernetes community abides by the CNCF code of conduct . Here is an excerpt: As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. Getting Started \u00b6 We have full documentation on how to get started contributing here: Contributor License Agreement Kubernetes projects require that you sign a Contributor License Agreement (CLA) before we can accept your pull requests Kubernetes Contributor Guide - Main contributor documentation, or you can just jump directly to the contributing section Contributor Cheat Sheet - Common resources for existing developers Mentorship \u00b6 Mentoring Initiatives - We have a diverse set of mentorship programs available that are always looking for volunteers! Contact Information \u00b6 Slack channel Mailing list","title":"Kubernetes Contributions"},{"location":"CONTRIBUTING/#contributing-guidelines","text":"Welcome to Kubernetes. We are excited about the prospect of you joining our community ! The Kubernetes community abides by the CNCF code of conduct . Here is an excerpt: As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.","title":"Contributing Guidelines"},{"location":"CONTRIBUTING/#getting-started","text":"We have full documentation on how to get started contributing here: Contributor License Agreement Kubernetes projects require that you sign a Contributor License Agreement (CLA) before we can accept your pull requests Kubernetes Contributor Guide - Main contributor documentation, or you can just jump directly to the contributing section Contributor Cheat Sheet - Common resources for existing developers","title":"Getting Started"},{"location":"CONTRIBUTING/#mentorship","text":"Mentoring Initiatives - We have a diverse set of mentorship programs available that are always looking for volunteers!","title":"Mentorship"},{"location":"CONTRIBUTING/#contact-information","text":"Slack channel Mailing list","title":"Contact Information"},{"location":"LICENSE/","text":"Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION Definitions. \u201cLicense\u201d shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. \u201cLicensor\u201d shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. \u201cLegal Entity\u201d shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, \u201ccontrol\u201d means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. \u201cYou\u201d (or \u201cYour\u201d) shall mean an individual or Legal Entity exercising permissions granted by this License. \u201cSource\u201d form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. \u201cObject\u201d form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. \u201cWork\u201d shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). \u201cDerivative Works\u201d shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. \u201cContribution\u201d shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, \u201csubmitted\u201d means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as \u201cNot a Contribution.\u201d \u201cContributor\u201d shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and \u00a9 You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a \u201cNOTICE\u201d text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an \u201cAS IS\u201d BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets \"{}\" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same \"printed page\" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the \u201cLicense\u201d); 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 \u201cAS IS\u201d 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.","title":"License"},{"location":"code-of-conduct/","text":"Kubernetes Community Code of Conduct \u00b6 Please refer to our Kubernetes Community Code of Conduct","title":"Code of Conduct"},{"location":"code-of-conduct/#kubernetes-community-code-of-conduct","text":"Please refer to our Kubernetes Community Code of Conduct","title":"Kubernetes Community Code of Conduct"},{"location":"faq/","text":"Frequently asked questions \u00b6 How is ExternalDNS useful to me? \u00b6 You\u2019ve probably created many deployments. Typically, you expose your deployment to the Internet by creating a Service with type=LoadBalancer . Depending on your environment, this usually assigns a random publicly available endpoint to your service that you can access from anywhere in the world. On Google Kubernetes Engine, this is a public IP address: $ kubectl get svc NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE nginx 10.3.249.226 35.187.104.85 80:32281/TCP 1m But dealing with IPs for service discovery isn\u2019t nice, so you register this IP with your DNS provider under a better name\u2014most likely, one that corresponds to your service name. If the IP changes, you update the DNS record accordingly. Those times are over! ExternalDNS takes care of that last step for you by keeping your DNS records synchronized with your external entry points. ExternalDNS\u2019 usefulness also becomes clear when you use Ingresses to allow external traffic into your cluster. Via Ingress, you can tell Kubernetes to route traffic to different services based on certain HTTP request attributes, e.g. the Host header: $ kubectl get ing NAME HOSTS ADDRESS PORTS AGE entrypoint frontend.example.org,backend.example.org 35.186.250.78 80 1m But there\u2019s nothing that actually makes clients resolve those hostnames to the Ingress\u2019 IP address. Again, you normally have to register each entry with your DNS provider. Only if you\u2019re lucky can you use a wildcard, like in the example above. ExternalDNS can solve this for you as well. Which DNS providers are supported? \u00b6 Please check the provider status table for the list of supported providers and their status. As stated in the README, we are currently looking for stable maintainers for those providers, to ensure that bugfixes and new features will be available for all of those. Which Kubernetes objects are supported? \u00b6 Services exposed via type=LoadBalancer , type=ExternalName , type=NodePort , and for the hostnames defined in Ingress objects as well as headless hostPort services. How do I specify a DNS name for my Kubernetes objects? \u00b6 There are three sources of information for ExternalDNS to decide on DNS name. ExternalDNS will pick one in order as listed below: For ingress objects ExternalDNS will create a DNS record based on the hosts specified for the ingress object, as well as the external-dns.alpha.kubernetes.io/hostname annotation. For services ExternalDNS will look for the annotation external-dns.alpha.kubernetes.io/hostname on the service and use the loadbalancer IP, it also will look for the annotation external-dns.alpha.kubernetes.io/internal-hostname on the service and use the service IP. For ingresses, you can optionally force ExternalDNS to create records based on either the hosts specified or the external-dns.alpha.kubernetes.io/hostname annotation. This behavior is controlled by setting the external-dns.alpha.kubernetes.io/ingress-hostname-source annotation on that ingress to either defined-hosts-only or annotation-only . If compatibility mode is enabled (e.g. --compatibility={mate,molecule} flag), External DNS will parse annotations used by Zalando/Mate, wearemolecule/route53-kubernetes. Compatibility mode with Kops DNS Controller is planned to be added in the future. If --fqdn-template flag is specified, e.g. --fqdn-template={{.Name}}.my-org.com , ExternalDNS will use service/ingress specifications for the provided template to generate DNS name. Can I specify multiple global FQDN templates? \u00b6 Yes, you can. Pass in a comma separated list to --fqdn-template . Beaware this will double (triple, etc) the amount of DNS entries based on how many services, ingresses and so on you have and will get you faster towards the API request limit of your DNS provider. Which Service and Ingress controllers are supported? \u00b6 Regarding Services, we\u2019ll support the OSI Layer 4 load balancers that Kubernetes creates on AWS and Google Kubernetes Engine, and possibly other clusters running on Google Compute Engine. Regarding Ingress, we\u2019ll support: * Google\u2019s Ingress Controller on GKE that integrates with their Layer 7 load balancers (GLBC) * nginx-ingress-controller v0.9.x with a fronting Service * Zalando\u2019s AWS Ingress controller , based on AWS ALBs and Skipper * Traefik * version 1.7, when kubernetes.ingressEndpoint is configured ( kubernetes.ingressEndpoint.useDefaultPublishedService in the Helm chart ) * versions >=2.0, when providers.kubernetesIngress.ingressEndpoint is configured ( providers.kubernetesIngress.publishedService.enabled is set to true in the new Helm chart ) Are other Ingress Controllers supported? \u00b6 For Ingress objects, ExternalDNS will attempt to discover the target hostname of the relevant Ingress Controller automatically. If you are using an Ingress Controller that is not listed above you may have issues with ExternalDNS not discovering Endpoints and consequently not creating any DNS records. As a workaround, it is possible to force create an Endpoint by manually specifying a target host/IP for the records to be created by setting the annotation external-dns.alpha.kubernetes.io/target in the Ingress object. Another reason you may want to override the ingress hostname or IP address is if you have an external mechanism for handling failover across ingress endpoints. Possible scenarios for this would include using keepalived-vip to manage failover faster than DNS TTLs might expire. Note that if you set the target to a hostname, then a CNAME record will be created. In this case, the hostname specified in the Ingress object\u2019s annotation must already exist. (i.e. you have a Service resource for your Ingress Controller with the external-dns.alpha.kubernetes.io/hostname annotation set to the same value.) What about other projects similar to ExternalDNS? \u00b6 ExternalDNS is a joint effort to unify different projects accomplishing the same goals, namely: Kops\u2019 DNS Controller Zalando\u2019s Mate Molecule Software\u2019s route53-kubernetes We strive to make the migration from these implementations a smooth experience. This means that, for some time, we\u2019ll support their annotation semantics in ExternalDNS and allow both implementations to run side-by-side. This enables you to migrate incrementally and slowly phase out the other implementation. How does it work with other implementations and legacy records? \u00b6 ExternalDNS will allow you to opt into any Services and Ingresses that you want it to consider, by an annotation. This way, it can co-exist with other implementations running in the same cluster if they also support this pattern. However, we\u2019ll most likely declare ExternalDNS to be the default implementation. This means that ExternalDNS will consider Services and Ingresses that don\u2019t specifically declare which controller they want to be processed by; this is similar to the ingress.class annotation on GKE. I\u2019m afraid you will mess up my DNS records! \u00b6 Since v0.3, ExternalDNS can be configured to use an ownership registry. When this option is enabled, ExternalDNS will keep track of which records it has control over, and will never modify any records over which it doesn\u2019t have control. This is a fundamental requirement to operate ExternalDNS safely when there might be other actors creating DNS records in the same target space. For now ExternalDNS uses TXT records to label owned records, and there might be other alternatives coming in the future releases. Does anyone use ExternalDNS in production? \u00b6 Yes, multiple companies are using ExternalDNS in production. Zalando, as an example, has been using it in production since its v0.3 release, mostly using the AWS provider. How can we start using ExternalDNS? \u00b6 Check out the following descriptive tutorials on how to run ExternalDNS in GKE and AWS or any other supported provider. Why is ExternalDNS only adding a single IP address in Route 53 on AWS when using the nginx-ingress-controller ? How do I get it to use the FQDN of the ELB assigned to my nginx-ingress-controller Service instead? \u00b6 By default the nginx-ingress-controller assigns a single IP address to an Ingress resource when it\u2019s created. ExternalDNS uses what\u2019s assigned to the Ingress resource, so it too will use this single IP address when adding the record in Route 53. In most AWS deployments, you\u2019ll instead want the Route 53 entry to be the FQDN of the ELB that is assigned to the nginx-ingress-controller Service. To accomplish this, when you create the nginx-ingress-controller Deployment, you need to provide the --publish-service option to the /nginx-ingress-controller executable under args . Once this is deployed new Ingress resources will get the ELB\u2019s FQDN and ExternalDNS will use the same when creating records in Route 53. According to the nginx-ingress-controller docs the value you need to provide --publish-service is: Service fronting the ingress controllers. Takes the form namespace/name. The controller will set the endpoint records on the ingress objects to reflect those on the service. For example if your nginx-ingress-controller Service\u2019s name is nginx-ingress-controller-svc and it\u2019s in the default namespace the start of your resource YAML might look like the following. Note the second to last line. apiVersion: apps/v1 kind: Deployment metadata: name: nginx-ingress-controller spec: replicas: 1 selector: matchLabels: app: nginx-ingress template: metadata: labels: app: nginx-ingress spec: hostNetwork: false containers: - name: nginx-ingress-controller image: \"gcr.io/google_containers/nginx-ingress-controller:0.9.0-beta.11\" imagePullPolicy: \"IfNotPresent\" args: - /nginx-ingress-controller - --default-backend-service={your-backend-service} - --publish-service=default/nginx-ingress-controller-svc - --configmap={your-configmap} I have a Service/Ingress but it\u2019s ignored by ExternalDNS. Why? \u00b6 ExternalDNS can be configured to only use Services or Ingresses as source. In case Services or Ingresses seem to be ignored in your setup, consider checking how the flag --source was configured when deployed. For reference, see the issue https://github.com/kubernetes-sigs/external-dns/issues/267. I\u2019m using an ELB with TXT registry but the CNAME record clashes with the TXT record. How to avoid this? \u00b6 CNAMEs cannot co-exist with other records, therefore you can use the --txt-prefix flag which makes sure to create a TXT record with a name following the pattern prefix. . For reference, see the issue https://github.com/kubernetes-sigs/external-dns/issues/262. Can I force ExternalDNS to create CNAME records for ELB/ALB? \u00b6 The default logic is: when a target looks like an ELB/ALB, ExternalDNS will create ALIAS records for it. Under certain circumstances you want to force ExternalDNS to create CNAME records instead. If you want to do that, start ExternalDNS with the --aws-prefer-cname flag. Why should I want to force ExternalDNS to create CNAME records for ELB/ALB? Some motivations of users were: \u201cOur hosted zones records are synchronized with our enterprise DNS. The record type ALIAS is an AWS proprietary record type and AWS allows you to set a DNS record directly on AWS resources. Since this is not a DNS RfC standard and therefore can not be transferred and created in our enterprise DNS. So we need to force CNAME creation instead.\u201d or \u201cIn case of ALIAS if we do nslookup with domain name, it will return only IPs of ELB. So it is always difficult for us to locate ELB in AWS console to which domain is pointing. If we configure it with CNAME it will return exact ELB CNAME, which is more helpful.!\u201d Which permissions do I need when running ExternalDNS on a GCE or GKE node. \u00b6 You need to add either https://www.googleapis.com/auth/ndev.clouddns.readwrite or https://www.googleapis.com/auth/cloud-platform on your instance group\u2019s scope. What metrics can I get from ExternalDNS and what do they mean? \u00b6 ExternalDNS exposes 2 types of metrics: Sources and Registry errors. Source s are mostly Kubernetes API objects. Examples of source errors may be connection errors to the Kubernetes API server itself or missing RBAC permissions. It can also stem from incompatible configuration in the objects itself like invalid characters, processing a broken fqdnTemplate, etc. Registry errors are mostly Provider errors, unless there\u2019s some coding flaw in the registry package. Provider errors often arise due to accessing their APIs due to network or missing cloud-provider permissions when reading records. When applying a changeset, errors will arise if the changeset applied is incompatible with the current state. In case of an increased error count, you could correlate them with the http_request_duration_seconds{handler=\"instrumented_http\"} metric which should show increased numbers for status codes 4xx (permissions, configuration, invalid changeset) or 5xx (apiserver down). You can use the host label in the metric to figure out if the request was against the Kubernetes API server (Source errors) or the DNS provider API (Registry/Provider errors). Here is the full list of available metrics provided by ExternalDNS: Name Description Type external_dns_controller_last_sync_timestamp_seconds Timestamp of last successful sync with the DNS provider Gauge external_dns_registry_endpoints_total Number of Endpoints in all sources Gauge external_dns_registry_errors_total Number of Registry errors Counter external_dns_source_endpoints_total Number of Endpoints in the registry Gauge external_dns_source_errors_total Number of Source errors Counter external_dns_controller_verified_aaaa_records Number of DNS AAAA-records that exists both in source and registry Gauge external_dns_controller_verified_a_records Number of DNS A-records that exists both in source and registry Gauge external_dns_registry_aaaa_records Number of AAAA records in registry Gauge external_dns_registry_a_records Number of A records in registry Gauge external_dns_source_aaaa_records Number of AAAA records in source Gauge external_dns_source_a_records Number of A records in source Gauge How can I run ExternalDNS under a specific GCP Service Account, e.g. to access DNS records in other projects? \u00b6 Have a look at https://github.com/linki/mate/blob/v0.6.2/examples/google/README.md#permissions How do I configure multiple Sources via environment variables? (also applies to domain filters) \u00b6 Separate the individual values via a line break. The equivalent of --source=service --source=ingress would be service\\ningress . However, it can be tricky do define that depending on your environment. The following examples work (zsh): Via docker: $ docker run \\ -e EXTERNAL_DNS_SOURCE = $'service\\ningress' \\ -e EXTERNAL_DNS_PROVIDER = google \\ -e EXTERNAL_DNS_DOMAIN_FILTER = $'foo.com\\nbar.com' \\ registry.k8s.io/external-dns/external-dns:v0.13.5 time=\"2017-08-08T14:10:26Z\" level=info msg=\"config: &{APIServerURL: KubeConfig: Sources:[service ingress] Namespace: ... Locally: $ export EXTERNAL_DNS_SOURCE = $'service\\ningress' $ external-dns --provider = google INFO[0000] config: &{APIServerURL: KubeConfig: Sources:[service ingress] Namespace: ... $ EXTERNAL_DNS_SOURCE=$'service\\ningress' external-dns --provider=google INFO[0000] config: &{APIServerURL: KubeConfig: Sources:[service ingress] Namespace: ... In a Kubernetes manifest: spec : containers : - name : external-dns args : - --provider=google env : - name : EXTERNAL_DNS_SOURCE value : \"service\\ningress\" Or preferably: spec : containers : - name : external-dns args : - --provider=google env : - name : EXTERNAL_DNS_SOURCE value : |- service ingress Running an internal and external dns service \u00b6 Sometimes you need to run an internal and an external dns service. The internal one should provision hostnames used on the internal network (perhaps inside a VPC), and the external one to expose DNS to the internet. To do this with ExternalDNS you can use the --ingress-class flag to specifically tie an instance of ExternalDNS to an instance of a ingress controller. Let\u2019s assume you have two ingress controllers, internal and external . You can then start two ExternalDNS providers, one with --ingress-class=internal and one with --ingress-class=external . If you need to search for multiple ingress classes, you can specify the flag multiple times, like so: --ingress-class=internal --ingress-class=external . The --ingress-class flag will check both the spec.ingressClassName field and the deprecated kubernetes.io/ingress.class annotation. The spec.ingressClassName tasks precedence over the annotation if both are supplied. Backward compatibility The previous --annotation-filter flag can still be used to restrict which objects ExternalDNS considers; for example, --annotation-filter=kubernetes.io/ingress.class in (public,dmz) . However, beware when using annotation filters with multiple sources, e.g. --source=service --source=ingress , since --annotation-filter will filter every given source object. If you need to use annotation filters against a specific source you have to run a separated external dns service containing only the wanted --source and --annotation-filter . Note: the --ingress-class flag cannot be used at the same time as the --annotation-filter=kubernetes.io/ingress.class in (...) flag; if you do this an error will be raised. Performance considerations Filtering based on ingress class name or annotations means that the external-dns controller will receive all resources of that kind and then filter on the client-side. In larger clusters with many resources which change frequently this can cause performance issues. If only some resources need to be managed by an instance of external-dns then label filtering can be used instead of ingress class filtering (or legacy annotation filtering). This means that only those resources which match the selector specified in --label-filter will be passed to the controller. How do I specify that I want the DNS record to point to either the Node\u2019s public or private IP when it has both? \u00b6 If your Nodes have both public and private IP addresses, you might want to write DNS records with one or the other. For example, you may want to write a DNS record in a private zone that resolves to your Nodes\u2019 private IPs so that traffic never leaves your private network. To accomplish this, set this annotation on your service: external-dns.alpha.kubernetes.io/access=private Conversely, to force the public IP: external-dns.alpha.kubernetes.io/access=public If this annotation is not set, and the node has both public and private IP addresses, then the public IP will be used by default. Some loadbalancer implementations assign multiple IP addresses as external addresses. You can filter the generated targets by their networks using --target-net-filter=10.0.0.0/8 or --exclude-target-net=10.0.0.0/8 . Can external-dns manage(add/remove) records in a hosted zone which is setup in different AWS account? \u00b6 Yes, give it the correct cross-account/assume-role permissions and use the --aws-assume-role flag https://github.com/kubernetes-sigs/external-dns/pull/524#issue-181256561 How do I provide multiple values to the annotation external-dns.alpha.kubernetes.io/hostname ? \u00b6 Separate them by , . Are there official Docker images provided? \u00b6 When we tag a new release, we push a container image to the Kubernetes projects official container registry with the following name: registry.k8s.io/external-dns/external-dns As tags, you use the external-dns release of choice(i.e. v0.7.6 ). A latest tag is not provided in the container registry. If you wish to build your own image, you can use the provided .ko.yaml as a starting point. Which architectures are supported? \u00b6 From v0.7.5 on we support amd64 , arm32v7 and arm64v8 . This means that you can run ExternalDNS on a Kubernetes cluster backed by Rasperry Pis or on ARM instances in the cloud as well as more traditional machines backed by amd64 compatible CPUs. Which operating systems are supported? \u00b6 At the time of writing we only support GNU/linux and we have no plans of supporting Windows or other operating systems. Why am I seeing time out errors even though I have connectivity to my cluster? \u00b6 If you\u2019re seeing an error such as this: FATA[0060] failed to sync cache: timed out waiting for the condition You may not have the correct permissions required to query all the necessary resources in your kubernetes cluster. Specifically, you may be running in a namespace that you don\u2019t have these permissions in. By default, commands are run against the default namespace. Try changing this to your particular namespace to see if that fixes the issue.","title":"FAQ"},{"location":"faq/#frequently-asked-questions","text":"","title":"Frequently asked questions"},{"location":"faq/#how-is-externaldns-useful-to-me","text":"You\u2019ve probably created many deployments. Typically, you expose your deployment to the Internet by creating a Service with type=LoadBalancer . Depending on your environment, this usually assigns a random publicly available endpoint to your service that you can access from anywhere in the world. On Google Kubernetes Engine, this is a public IP address: $ kubectl get svc NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE nginx 10.3.249.226 35.187.104.85 80:32281/TCP 1m But dealing with IPs for service discovery isn\u2019t nice, so you register this IP with your DNS provider under a better name\u2014most likely, one that corresponds to your service name. If the IP changes, you update the DNS record accordingly. Those times are over! ExternalDNS takes care of that last step for you by keeping your DNS records synchronized with your external entry points. ExternalDNS\u2019 usefulness also becomes clear when you use Ingresses to allow external traffic into your cluster. Via Ingress, you can tell Kubernetes to route traffic to different services based on certain HTTP request attributes, e.g. the Host header: $ kubectl get ing NAME HOSTS ADDRESS PORTS AGE entrypoint frontend.example.org,backend.example.org 35.186.250.78 80 1m But there\u2019s nothing that actually makes clients resolve those hostnames to the Ingress\u2019 IP address. Again, you normally have to register each entry with your DNS provider. Only if you\u2019re lucky can you use a wildcard, like in the example above. ExternalDNS can solve this for you as well.","title":"How is ExternalDNS useful to me?"},{"location":"faq/#which-dns-providers-are-supported","text":"Please check the provider status table for the list of supported providers and their status. As stated in the README, we are currently looking for stable maintainers for those providers, to ensure that bugfixes and new features will be available for all of those.","title":"Which DNS providers are supported?"},{"location":"faq/#which-kubernetes-objects-are-supported","text":"Services exposed via type=LoadBalancer , type=ExternalName , type=NodePort , and for the hostnames defined in Ingress objects as well as headless hostPort services.","title":"Which Kubernetes objects are supported?"},{"location":"faq/#how-do-i-specify-a-dns-name-for-my-kubernetes-objects","text":"There are three sources of information for ExternalDNS to decide on DNS name. ExternalDNS will pick one in order as listed below: For ingress objects ExternalDNS will create a DNS record based on the hosts specified for the ingress object, as well as the external-dns.alpha.kubernetes.io/hostname annotation. For services ExternalDNS will look for the annotation external-dns.alpha.kubernetes.io/hostname on the service and use the loadbalancer IP, it also will look for the annotation external-dns.alpha.kubernetes.io/internal-hostname on the service and use the service IP. For ingresses, you can optionally force ExternalDNS to create records based on either the hosts specified or the external-dns.alpha.kubernetes.io/hostname annotation. This behavior is controlled by setting the external-dns.alpha.kubernetes.io/ingress-hostname-source annotation on that ingress to either defined-hosts-only or annotation-only . If compatibility mode is enabled (e.g. --compatibility={mate,molecule} flag), External DNS will parse annotations used by Zalando/Mate, wearemolecule/route53-kubernetes. Compatibility mode with Kops DNS Controller is planned to be added in the future. If --fqdn-template flag is specified, e.g. --fqdn-template={{.Name}}.my-org.com , ExternalDNS will use service/ingress specifications for the provided template to generate DNS name.","title":"How do I specify a DNS name for my Kubernetes objects?"},{"location":"faq/#can-i-specify-multiple-global-fqdn-templates","text":"Yes, you can. Pass in a comma separated list to --fqdn-template . Beaware this will double (triple, etc) the amount of DNS entries based on how many services, ingresses and so on you have and will get you faster towards the API request limit of your DNS provider.","title":"Can I specify multiple global FQDN templates?"},{"location":"faq/#which-service-and-ingress-controllers-are-supported","text":"Regarding Services, we\u2019ll support the OSI Layer 4 load balancers that Kubernetes creates on AWS and Google Kubernetes Engine, and possibly other clusters running on Google Compute Engine. Regarding Ingress, we\u2019ll support: * Google\u2019s Ingress Controller on GKE that integrates with their Layer 7 load balancers (GLBC) * nginx-ingress-controller v0.9.x with a fronting Service * Zalando\u2019s AWS Ingress controller , based on AWS ALBs and Skipper * Traefik * version 1.7, when kubernetes.ingressEndpoint is configured ( kubernetes.ingressEndpoint.useDefaultPublishedService in the Helm chart ) * versions >=2.0, when providers.kubernetesIngress.ingressEndpoint is configured ( providers.kubernetesIngress.publishedService.enabled is set to true in the new Helm chart )","title":"Which Service and Ingress controllers are supported?"},{"location":"faq/#are-other-ingress-controllers-supported","text":"For Ingress objects, ExternalDNS will attempt to discover the target hostname of the relevant Ingress Controller automatically. If you are using an Ingress Controller that is not listed above you may have issues with ExternalDNS not discovering Endpoints and consequently not creating any DNS records. As a workaround, it is possible to force create an Endpoint by manually specifying a target host/IP for the records to be created by setting the annotation external-dns.alpha.kubernetes.io/target in the Ingress object. Another reason you may want to override the ingress hostname or IP address is if you have an external mechanism for handling failover across ingress endpoints. Possible scenarios for this would include using keepalived-vip to manage failover faster than DNS TTLs might expire. Note that if you set the target to a hostname, then a CNAME record will be created. In this case, the hostname specified in the Ingress object\u2019s annotation must already exist. (i.e. you have a Service resource for your Ingress Controller with the external-dns.alpha.kubernetes.io/hostname annotation set to the same value.)","title":"Are other Ingress Controllers supported?"},{"location":"faq/#what-about-other-projects-similar-to-externaldns","text":"ExternalDNS is a joint effort to unify different projects accomplishing the same goals, namely: Kops\u2019 DNS Controller Zalando\u2019s Mate Molecule Software\u2019s route53-kubernetes We strive to make the migration from these implementations a smooth experience. This means that, for some time, we\u2019ll support their annotation semantics in ExternalDNS and allow both implementations to run side-by-side. This enables you to migrate incrementally and slowly phase out the other implementation.","title":"What about other projects similar to ExternalDNS?"},{"location":"faq/#how-does-it-work-with-other-implementations-and-legacy-records","text":"ExternalDNS will allow you to opt into any Services and Ingresses that you want it to consider, by an annotation. This way, it can co-exist with other implementations running in the same cluster if they also support this pattern. However, we\u2019ll most likely declare ExternalDNS to be the default implementation. This means that ExternalDNS will consider Services and Ingresses that don\u2019t specifically declare which controller they want to be processed by; this is similar to the ingress.class annotation on GKE.","title":"How does it work with other implementations and legacy records?"},{"location":"faq/#im-afraid-you-will-mess-up-my-dns-records","text":"Since v0.3, ExternalDNS can be configured to use an ownership registry. When this option is enabled, ExternalDNS will keep track of which records it has control over, and will never modify any records over which it doesn\u2019t have control. This is a fundamental requirement to operate ExternalDNS safely when there might be other actors creating DNS records in the same target space. For now ExternalDNS uses TXT records to label owned records, and there might be other alternatives coming in the future releases.","title":"I'm afraid you will mess up my DNS records!"},{"location":"faq/#does-anyone-use-externaldns-in-production","text":"Yes, multiple companies are using ExternalDNS in production. Zalando, as an example, has been using it in production since its v0.3 release, mostly using the AWS provider.","title":"Does anyone use ExternalDNS in production?"},{"location":"faq/#how-can-we-start-using-externaldns","text":"Check out the following descriptive tutorials on how to run ExternalDNS in GKE and AWS or any other supported provider.","title":"How can we start using ExternalDNS?"},{"location":"faq/#why-is-externaldns-only-adding-a-single-ip-address-in-route-53-on-aws-when-using-the-nginx-ingress-controller-how-do-i-get-it-to-use-the-fqdn-of-the-elb-assigned-to-my-nginx-ingress-controller-service-instead","text":"By default the nginx-ingress-controller assigns a single IP address to an Ingress resource when it\u2019s created. ExternalDNS uses what\u2019s assigned to the Ingress resource, so it too will use this single IP address when adding the record in Route 53. In most AWS deployments, you\u2019ll instead want the Route 53 entry to be the FQDN of the ELB that is assigned to the nginx-ingress-controller Service. To accomplish this, when you create the nginx-ingress-controller Deployment, you need to provide the --publish-service option to the /nginx-ingress-controller executable under args . Once this is deployed new Ingress resources will get the ELB\u2019s FQDN and ExternalDNS will use the same when creating records in Route 53. According to the nginx-ingress-controller docs the value you need to provide --publish-service is: Service fronting the ingress controllers. Takes the form namespace/name. The controller will set the endpoint records on the ingress objects to reflect those on the service. For example if your nginx-ingress-controller Service\u2019s name is nginx-ingress-controller-svc and it\u2019s in the default namespace the start of your resource YAML might look like the following. Note the second to last line. apiVersion: apps/v1 kind: Deployment metadata: name: nginx-ingress-controller spec: replicas: 1 selector: matchLabels: app: nginx-ingress template: metadata: labels: app: nginx-ingress spec: hostNetwork: false containers: - name: nginx-ingress-controller image: \"gcr.io/google_containers/nginx-ingress-controller:0.9.0-beta.11\" imagePullPolicy: \"IfNotPresent\" args: - /nginx-ingress-controller - --default-backend-service={your-backend-service} - --publish-service=default/nginx-ingress-controller-svc - --configmap={your-configmap}","title":"Why is ExternalDNS only adding a single IP address in Route 53 on AWS when using the nginx-ingress-controller? How do I get it to use the FQDN of the ELB assigned to my nginx-ingress-controller Service instead?"},{"location":"faq/#i-have-a-serviceingress-but-its-ignored-by-externaldns-why","text":"ExternalDNS can be configured to only use Services or Ingresses as source. In case Services or Ingresses seem to be ignored in your setup, consider checking how the flag --source was configured when deployed. For reference, see the issue https://github.com/kubernetes-sigs/external-dns/issues/267.","title":"I have a Service/Ingress but it's ignored by ExternalDNS. Why?"},{"location":"faq/#im-using-an-elb-with-txt-registry-but-the-cname-record-clashes-with-the-txt-record-how-to-avoid-this","text":"CNAMEs cannot co-exist with other records, therefore you can use the --txt-prefix flag which makes sure to create a TXT record with a name following the pattern prefix. . For reference, see the issue https://github.com/kubernetes-sigs/external-dns/issues/262.","title":"I'm using an ELB with TXT registry but the CNAME record clashes with the TXT record. How to avoid this?"},{"location":"faq/#can-i-force-externaldns-to-create-cname-records-for-elbalb","text":"The default logic is: when a target looks like an ELB/ALB, ExternalDNS will create ALIAS records for it. Under certain circumstances you want to force ExternalDNS to create CNAME records instead. If you want to do that, start ExternalDNS with the --aws-prefer-cname flag. Why should I want to force ExternalDNS to create CNAME records for ELB/ALB? Some motivations of users were: \u201cOur hosted zones records are synchronized with our enterprise DNS. The record type ALIAS is an AWS proprietary record type and AWS allows you to set a DNS record directly on AWS resources. Since this is not a DNS RfC standard and therefore can not be transferred and created in our enterprise DNS. So we need to force CNAME creation instead.\u201d or \u201cIn case of ALIAS if we do nslookup with domain name, it will return only IPs of ELB. So it is always difficult for us to locate ELB in AWS console to which domain is pointing. If we configure it with CNAME it will return exact ELB CNAME, which is more helpful.!\u201d","title":"Can I force ExternalDNS to create CNAME records for ELB/ALB?"},{"location":"faq/#which-permissions-do-i-need-when-running-externaldns-on-a-gce-or-gke-node","text":"You need to add either https://www.googleapis.com/auth/ndev.clouddns.readwrite or https://www.googleapis.com/auth/cloud-platform on your instance group\u2019s scope.","title":"Which permissions do I need when running ExternalDNS on a GCE or GKE node."},{"location":"faq/#what-metrics-can-i-get-from-externaldns-and-what-do-they-mean","text":"ExternalDNS exposes 2 types of metrics: Sources and Registry errors. Source s are mostly Kubernetes API objects. Examples of source errors may be connection errors to the Kubernetes API server itself or missing RBAC permissions. It can also stem from incompatible configuration in the objects itself like invalid characters, processing a broken fqdnTemplate, etc. Registry errors are mostly Provider errors, unless there\u2019s some coding flaw in the registry package. Provider errors often arise due to accessing their APIs due to network or missing cloud-provider permissions when reading records. When applying a changeset, errors will arise if the changeset applied is incompatible with the current state. In case of an increased error count, you could correlate them with the http_request_duration_seconds{handler=\"instrumented_http\"} metric which should show increased numbers for status codes 4xx (permissions, configuration, invalid changeset) or 5xx (apiserver down). You can use the host label in the metric to figure out if the request was against the Kubernetes API server (Source errors) or the DNS provider API (Registry/Provider errors). Here is the full list of available metrics provided by ExternalDNS: Name Description Type external_dns_controller_last_sync_timestamp_seconds Timestamp of last successful sync with the DNS provider Gauge external_dns_registry_endpoints_total Number of Endpoints in all sources Gauge external_dns_registry_errors_total Number of Registry errors Counter external_dns_source_endpoints_total Number of Endpoints in the registry Gauge external_dns_source_errors_total Number of Source errors Counter external_dns_controller_verified_aaaa_records Number of DNS AAAA-records that exists both in source and registry Gauge external_dns_controller_verified_a_records Number of DNS A-records that exists both in source and registry Gauge external_dns_registry_aaaa_records Number of AAAA records in registry Gauge external_dns_registry_a_records Number of A records in registry Gauge external_dns_source_aaaa_records Number of AAAA records in source Gauge external_dns_source_a_records Number of A records in source Gauge","title":"What metrics can I get from ExternalDNS and what do they mean?"},{"location":"faq/#how-can-i-run-externaldns-under-a-specific-gcp-service-account-eg-to-access-dns-records-in-other-projects","text":"Have a look at https://github.com/linki/mate/blob/v0.6.2/examples/google/README.md#permissions","title":"How can I run ExternalDNS under a specific GCP Service Account, e.g. to access DNS records in other projects?"},{"location":"faq/#how-do-i-configure-multiple-sources-via-environment-variables-also-applies-to-domain-filters","text":"Separate the individual values via a line break. The equivalent of --source=service --source=ingress would be service\\ningress . However, it can be tricky do define that depending on your environment. The following examples work (zsh): Via docker: $ docker run \\ -e EXTERNAL_DNS_SOURCE = $'service\\ningress' \\ -e EXTERNAL_DNS_PROVIDER = google \\ -e EXTERNAL_DNS_DOMAIN_FILTER = $'foo.com\\nbar.com' \\ registry.k8s.io/external-dns/external-dns:v0.13.5 time=\"2017-08-08T14:10:26Z\" level=info msg=\"config: &{APIServerURL: KubeConfig: Sources:[service ingress] Namespace: ... Locally: $ export EXTERNAL_DNS_SOURCE = $'service\\ningress' $ external-dns --provider = google INFO[0000] config: &{APIServerURL: KubeConfig: Sources:[service ingress] Namespace: ... $ EXTERNAL_DNS_SOURCE=$'service\\ningress' external-dns --provider=google INFO[0000] config: &{APIServerURL: KubeConfig: Sources:[service ingress] Namespace: ... In a Kubernetes manifest: spec : containers : - name : external-dns args : - --provider=google env : - name : EXTERNAL_DNS_SOURCE value : \"service\\ningress\" Or preferably: spec : containers : - name : external-dns args : - --provider=google env : - name : EXTERNAL_DNS_SOURCE value : |- service ingress","title":"How do I configure multiple Sources via environment variables? (also applies to domain filters)"},{"location":"faq/#running-an-internal-and-external-dns-service","text":"Sometimes you need to run an internal and an external dns service. The internal one should provision hostnames used on the internal network (perhaps inside a VPC), and the external one to expose DNS to the internet. To do this with ExternalDNS you can use the --ingress-class flag to specifically tie an instance of ExternalDNS to an instance of a ingress controller. Let\u2019s assume you have two ingress controllers, internal and external . You can then start two ExternalDNS providers, one with --ingress-class=internal and one with --ingress-class=external . If you need to search for multiple ingress classes, you can specify the flag multiple times, like so: --ingress-class=internal --ingress-class=external . The --ingress-class flag will check both the spec.ingressClassName field and the deprecated kubernetes.io/ingress.class annotation. The spec.ingressClassName tasks precedence over the annotation if both are supplied. Backward compatibility The previous --annotation-filter flag can still be used to restrict which objects ExternalDNS considers; for example, --annotation-filter=kubernetes.io/ingress.class in (public,dmz) . However, beware when using annotation filters with multiple sources, e.g. --source=service --source=ingress , since --annotation-filter will filter every given source object. If you need to use annotation filters against a specific source you have to run a separated external dns service containing only the wanted --source and --annotation-filter . Note: the --ingress-class flag cannot be used at the same time as the --annotation-filter=kubernetes.io/ingress.class in (...) flag; if you do this an error will be raised. Performance considerations Filtering based on ingress class name or annotations means that the external-dns controller will receive all resources of that kind and then filter on the client-side. In larger clusters with many resources which change frequently this can cause performance issues. If only some resources need to be managed by an instance of external-dns then label filtering can be used instead of ingress class filtering (or legacy annotation filtering). This means that only those resources which match the selector specified in --label-filter will be passed to the controller.","title":"Running an internal and external dns service"},{"location":"faq/#how-do-i-specify-that-i-want-the-dns-record-to-point-to-either-the-nodes-public-or-private-ip-when-it-has-both","text":"If your Nodes have both public and private IP addresses, you might want to write DNS records with one or the other. For example, you may want to write a DNS record in a private zone that resolves to your Nodes\u2019 private IPs so that traffic never leaves your private network. To accomplish this, set this annotation on your service: external-dns.alpha.kubernetes.io/access=private Conversely, to force the public IP: external-dns.alpha.kubernetes.io/access=public If this annotation is not set, and the node has both public and private IP addresses, then the public IP will be used by default. Some loadbalancer implementations assign multiple IP addresses as external addresses. You can filter the generated targets by their networks using --target-net-filter=10.0.0.0/8 or --exclude-target-net=10.0.0.0/8 .","title":"How do I specify that I want the DNS record to point to either the Node's public or private IP when it has both?"},{"location":"faq/#can-external-dns-manageaddremove-records-in-a-hosted-zone-which-is-setup-in-different-aws-account","text":"Yes, give it the correct cross-account/assume-role permissions and use the --aws-assume-role flag https://github.com/kubernetes-sigs/external-dns/pull/524#issue-181256561","title":"Can external-dns manage(add/remove) records in a hosted zone which is setup in different AWS account?"},{"location":"faq/#how-do-i-provide-multiple-values-to-the-annotation-external-dnsalphakubernetesiohostname","text":"Separate them by , .","title":"How do I provide multiple values to the annotation external-dns.alpha.kubernetes.io/hostname?"},{"location":"faq/#are-there-official-docker-images-provided","text":"When we tag a new release, we push a container image to the Kubernetes projects official container registry with the following name: registry.k8s.io/external-dns/external-dns As tags, you use the external-dns release of choice(i.e. v0.7.6 ). A latest tag is not provided in the container registry. If you wish to build your own image, you can use the provided .ko.yaml as a starting point.","title":"Are there official Docker images provided?"},{"location":"faq/#which-architectures-are-supported","text":"From v0.7.5 on we support amd64 , arm32v7 and arm64v8 . This means that you can run ExternalDNS on a Kubernetes cluster backed by Rasperry Pis or on ARM instances in the cloud as well as more traditional machines backed by amd64 compatible CPUs.","title":"Which architectures are supported?"},{"location":"faq/#which-operating-systems-are-supported","text":"At the time of writing we only support GNU/linux and we have no plans of supporting Windows or other operating systems.","title":"Which operating systems are supported?"},{"location":"faq/#why-am-i-seeing-time-out-errors-even-though-i-have-connectivity-to-my-cluster","text":"If you\u2019re seeing an error such as this: FATA[0060] failed to sync cache: timed out waiting for the condition You may not have the correct permissions required to query all the necessary resources in your kubernetes cluster. Specifically, you may be running in a namespace that you don\u2019t have these permissions in. By default, commands are run against the default namespace. Try changing this to your particular namespace to see if that fixes the issue.","title":"Why am I seeing time out errors even though I have connectivity to my cluster?"},{"location":"initial-design/","text":"Proposal: Design of External DNS \u00b6 Background \u00b6 Project proposal Initial discussion This document describes the initial design proposal. External DNS is purposed to fill the existing gap of creating DNS records for Kubernetes resources. While there exist alternative solutions, this project is meant to be a standard way of managing DNS records for Kubernetes. The current project is a fusion of the following projects and driven by its maintainers: Kops DNS Controller Mate wearemolecule/route53-kubernetes Example use case: \u00b6 User runs kubectl create -f ingress.yaml , this will create an ingress as normal. Typically the user would then have to manually create a DNS record pointing the ingress endpoint If the external-dns controller is running on the cluster, it could automatically configure the DNS records instead, by observing the host attribute in the ingress object. Goals \u00b6 Support AWS Route53 and Google Cloud DNS providers DNS for Kubernetes services(type=Loadbalancer) and Ingress Create/update/remove records as according to Kubernetes resources state It should address main requirements and support main features of the projects mentioned above Design \u00b6 Extensibility \u00b6 New cloud providers should be easily pluggable. Initially only AWS/Google platforms are supported. However, in the future we are planning to incorporate CoreDNS and Azure DNS as possible DNS providers Configuration \u00b6 DNS records will be automatically created in multiple situations: 1. Setting spec.rules.host on an ingress object. 2. Setting spec.tls.hosts on an ingress object. 3. Adding the annotation external-dns.alpha.kubernetes.io/hostname on an ingress object. 4. Adding the annotation external-dns.alpha.kubernetes.io/hostname on a type=LoadBalancer service object. Annotations \u00b6 Record configuration should occur via resource annotations. Supported annotations: Annotations Tag external-dns.alpha.kubernetes.io/controller Description Tells a DNS controller to process this service. This is useful when running different DNS controllers at the same time (or different versions of the same controller). The v1 implementation of dns-controller would look for service annotations dns-controller and dns-controller/v1 but not for mate/v1 or dns-controller/v2 Default dns-controller Example dns-controller/v1 Required false \u2014 \u2014 Tag external-dns.alpha.kubernetes.io/hostname Description Fully qualified name of the desired record Default none Example foo.example.org Required Only for services. Ingress hostname is retrieved from spec.rules.host meta data on ingress Compatibility \u00b6 External DNS should be compatible with annotations used by three above mentioned projects. The idea is that resources created and tagged with annotations for other projects should continue to be valid and now managed by External DNS. Mate Mate does not require services/ingress to be tagged. Therefore, it is not safe to run both Mate and External-DNS simultaneously. The idea is that initial release (?) of External DNS will support Mate annotations, which indicates the hostname to be created. Therefore the switch should be simple. Annotations Tag zalando.org/dnsname Description Hostname to be registered Default Empty(falls back to template based approach) Example foo.example.org Required false route53-kubernetes It should be safe to run both route53-kubernetes and external-dns simultaneously. Since route53-kubernetes only looks at services with the label dns=route53 and does not support ingress there should be no collisions between annotations. If users desire to switch to external-dns they can run both controllers and migrate services over as they are able. Ownership \u00b6 External DNS should be responsible for the created records. Which means that the records should be tagged and only tagged records are viable for future deletion/update. It should not mess with pre-existing records created via other means. Ownership via TXT records \u00b6 Each record managed by External DNS is accompanied with a TXT record with a specific value to indicate that corresponding DNS record is managed by External DNS and it can be updated/deleted respectively. TXT records are limited to lifetimes of service/ingress objects and are created/deleted once k8s resources are created/deleted.","title":"Initial Design"},{"location":"initial-design/#proposal-design-of-external-dns","text":"","title":"Proposal: Design of External DNS"},{"location":"initial-design/#background","text":"Project proposal Initial discussion This document describes the initial design proposal. External DNS is purposed to fill the existing gap of creating DNS records for Kubernetes resources. While there exist alternative solutions, this project is meant to be a standard way of managing DNS records for Kubernetes. The current project is a fusion of the following projects and driven by its maintainers: Kops DNS Controller Mate wearemolecule/route53-kubernetes","title":"Background"},{"location":"initial-design/#example-use-case","text":"User runs kubectl create -f ingress.yaml , this will create an ingress as normal. Typically the user would then have to manually create a DNS record pointing the ingress endpoint If the external-dns controller is running on the cluster, it could automatically configure the DNS records instead, by observing the host attribute in the ingress object.","title":"Example use case:"},{"location":"initial-design/#goals","text":"Support AWS Route53 and Google Cloud DNS providers DNS for Kubernetes services(type=Loadbalancer) and Ingress Create/update/remove records as according to Kubernetes resources state It should address main requirements and support main features of the projects mentioned above","title":"Goals"},{"location":"initial-design/#design","text":"","title":"Design"},{"location":"initial-design/#extensibility","text":"New cloud providers should be easily pluggable. Initially only AWS/Google platforms are supported. However, in the future we are planning to incorporate CoreDNS and Azure DNS as possible DNS providers","title":"Extensibility"},{"location":"initial-design/#configuration","text":"DNS records will be automatically created in multiple situations: 1. Setting spec.rules.host on an ingress object. 2. Setting spec.tls.hosts on an ingress object. 3. Adding the annotation external-dns.alpha.kubernetes.io/hostname on an ingress object. 4. Adding the annotation external-dns.alpha.kubernetes.io/hostname on a type=LoadBalancer service object.","title":"Configuration"},{"location":"initial-design/#annotations","text":"Record configuration should occur via resource annotations. Supported annotations: Annotations Tag external-dns.alpha.kubernetes.io/controller Description Tells a DNS controller to process this service. This is useful when running different DNS controllers at the same time (or different versions of the same controller). The v1 implementation of dns-controller would look for service annotations dns-controller and dns-controller/v1 but not for mate/v1 or dns-controller/v2 Default dns-controller Example dns-controller/v1 Required false \u2014 \u2014 Tag external-dns.alpha.kubernetes.io/hostname Description Fully qualified name of the desired record Default none Example foo.example.org Required Only for services. Ingress hostname is retrieved from spec.rules.host meta data on ingress","title":"Annotations"},{"location":"initial-design/#compatibility","text":"External DNS should be compatible with annotations used by three above mentioned projects. The idea is that resources created and tagged with annotations for other projects should continue to be valid and now managed by External DNS. Mate Mate does not require services/ingress to be tagged. Therefore, it is not safe to run both Mate and External-DNS simultaneously. The idea is that initial release (?) of External DNS will support Mate annotations, which indicates the hostname to be created. Therefore the switch should be simple. Annotations Tag zalando.org/dnsname Description Hostname to be registered Default Empty(falls back to template based approach) Example foo.example.org Required false route53-kubernetes It should be safe to run both route53-kubernetes and external-dns simultaneously. Since route53-kubernetes only looks at services with the label dns=route53 and does not support ingress there should be no collisions between annotations. If users desire to switch to external-dns they can run both controllers and migrate services over as they are able.","title":"Compatibility"},{"location":"initial-design/#ownership","text":"External DNS should be responsible for the created records. Which means that the records should be tagged and only tagged records are viable for future deletion/update. It should not mess with pre-existing records created via other means.","title":"Ownership"},{"location":"initial-design/#ownership-via-txt-records","text":"Each record managed by External DNS is accompanied with a TXT record with a specific value to indicate that corresponding DNS record is managed by External DNS and it can be updated/deleted respectively. TXT records are limited to lifetimes of service/ingress objects and are created/deleted once k8s resources are created/deleted.","title":"Ownership via TXT records"},{"location":"release/","text":"Release \u00b6 Release cycle \u00b6 Currently we don\u2019t release regularly. Whenever we think it makes sense to release a new version we do it, but we aim to do a new release every month. You might want to ask in our Slack channel external-dns when the next release will come out. Versioning convention \u00b6 These are the conventions that we will be using for releases following 0.7.6 : Patch version should be updated if we need to merge bugfixes, e.g. provider a does need a fix in order make updates working again. I would see updating or improving documentation here. Minor version should be updated if new features are implemented in existing providers or new provider get introduced. Major version should be upgraded if we introduce breaking changes. How to release a new image \u00b6 Prerequisite \u00b6 We use https://github.com/cli/cli to automate the release process. Please install it according to the official documentation . You must be an official maintainer of the project to be able to do a release. Steps \u00b6 Run scripts/releaser.sh to create a new GitHub release. Alternatively you can create a release in the GitHub UI making sure to click on the autogenerate release node feature. The step above will trigger the Kubernetes based CI/CD system Prow . Verify that a new image was built and uploaded to gcr.io/k8s-staging-external-dns/external-dns . Create a PR in the k8s.io repo (see https://github.com/kubernetes/k8s.io/pull/540 for reference) by taking the current staging image using the sha256 digest. Once the PR is merged, the image will be live with the corresponding tag specified in the PR. Verify that the image is pullable with the given tag (i.e. v0.7.5 ). Branch out from the default branch and run scripts/kustomize-version-updater.sh to update the image tag used in the kustomization.yaml. Create an issue to release the corresponding Helm chart via the chart release process (below) assigned to a chart maintainer Create a PR with the kustomize change. Create a PR to replace all versions for docker images in the tutorials. A possible script to use is sd registry.k8s.io/external-dns/external-dns:.* registry.k8s.io/external-dns/external-dns:v0.13.2 $(fd --type file) which uses the fd and sd utilities. Once the PR is merged, all is done :-) How to release a new chart version \u00b6 The chart needs to be released in response to an ExternalDNS image release or on an as-needed basis; this should be triggered by an issue to release the chart. Steps \u00b6 Create a PR to update Chart.yaml with the ExternalDNS version in appVersion , agreed on chart release version in version and annotations showing the changes Validate that the chart linting is successful Merge the PR to trigger a GitHub action to release the chart","title":"Release"},{"location":"release/#release","text":"","title":"Release"},{"location":"release/#release-cycle","text":"Currently we don\u2019t release regularly. Whenever we think it makes sense to release a new version we do it, but we aim to do a new release every month. You might want to ask in our Slack channel external-dns when the next release will come out.","title":"Release cycle"},{"location":"release/#versioning-convention","text":"These are the conventions that we will be using for releases following 0.7.6 : Patch version should be updated if we need to merge bugfixes, e.g. provider a does need a fix in order make updates working again. I would see updating or improving documentation here. Minor version should be updated if new features are implemented in existing providers or new provider get introduced. Major version should be upgraded if we introduce breaking changes.","title":"Versioning convention"},{"location":"release/#how-to-release-a-new-image","text":"","title":"How to release a new image"},{"location":"release/#prerequisite","text":"We use https://github.com/cli/cli to automate the release process. Please install it according to the official documentation . You must be an official maintainer of the project to be able to do a release.","title":"Prerequisite"},{"location":"release/#steps","text":"Run scripts/releaser.sh to create a new GitHub release. Alternatively you can create a release in the GitHub UI making sure to click on the autogenerate release node feature. The step above will trigger the Kubernetes based CI/CD system Prow . Verify that a new image was built and uploaded to gcr.io/k8s-staging-external-dns/external-dns . Create a PR in the k8s.io repo (see https://github.com/kubernetes/k8s.io/pull/540 for reference) by taking the current staging image using the sha256 digest. Once the PR is merged, the image will be live with the corresponding tag specified in the PR. Verify that the image is pullable with the given tag (i.e. v0.7.5 ). Branch out from the default branch and run scripts/kustomize-version-updater.sh to update the image tag used in the kustomization.yaml. Create an issue to release the corresponding Helm chart via the chart release process (below) assigned to a chart maintainer Create a PR with the kustomize change. Create a PR to replace all versions for docker images in the tutorials. A possible script to use is sd registry.k8s.io/external-dns/external-dns:.* registry.k8s.io/external-dns/external-dns:v0.13.2 $(fd --type file) which uses the fd and sd utilities. Once the PR is merged, all is done :-)","title":"Steps"},{"location":"release/#how-to-release-a-new-chart-version","text":"The chart needs to be released in response to an ExternalDNS image release or on an as-needed basis; this should be triggered by an issue to release the chart.","title":"How to release a new chart version"},{"location":"release/#steps_1","text":"Create a PR to update Chart.yaml with the ExternalDNS version in appVersion , agreed on chart release version in version and annotations showing the changes Validate that the chart linting is successful Merge the PR to trigger a GitHub action to release the chart","title":"Steps"},{"location":"ttl/","text":"Configure DNS record TTL (Time-To-Live) \u00b6 An optional annotation external-dns.alpha.kubernetes.io/ttl is available to customize the TTL value of a DNS record. TTL is specified as an integer encoded as string representing seconds. To configure it, simply annotate a service/ingress, e.g.: apiVersion : v1 kind : Service metadata : annotations : external-dns.alpha.kubernetes.io/hostname : nginx.external-dns-test.my-org.com. external-dns.alpha.kubernetes.io/ttl : \"60\" ... TTL can also be specified as a duration value parsable by Golang time.ParseDuration : apiVersion : v1 kind : Service metadata : annotations : external-dns.alpha.kubernetes.io/hostname : nginx.external-dns-test.my-org.com. external-dns.alpha.kubernetes.io/ttl : \"1m\" ... Both examples result in the same value of 60 seconds TTL. TTL must be a positive value. Providers \u00b6 AWS (Route53) Azure Cloudflare DigitalOcean DNSimple Google InMemory Linode TransIP RFC2136 Vultr UltraDNS PRs welcome! Notes \u00b6 When the external-dns.alpha.kubernetes.io/ttl annotation is not provided, the TTL will default to 0 seconds and endpoint.TTL.isConfigured() will be false. AWS Provider \u00b6 The AWS Provider overrides the value to 300s when the TTL is 0. This value is a constant in the provider code. Azure \u00b6 TTL value should be between 1 and 2,147,483,647 seconds. By default it will be 300s. CloudFlare Provider \u00b6 CloudFlare overrides the value to \u201cauto\u201d when the TTL is 0. DigitalOcean Provider \u00b6 The DigitalOcean Provider overrides the value to 300s when the TTL is 0. This value is a constant in the provider code. DNSimple Provider \u00b6 The DNSimple Provider default TTL is used when the TTL is 0. The default TTL is 3600s. Google Provider \u00b6 Previously with the Google Provider, TTL\u2019s were hard-coded to 300s. For safety, the Google Provider overrides the value to 300s when the TTL is 0. This value is a constant in the provider code. For the moment, it is impossible to use a TTL value of 0 with the AWS, DigitalOcean, or Google Providers. This behavior may change in the future. Linode Provider \u00b6 The Linode Provider default TTL is used when the TTL is 0. The default is 24 hours TransIP Provider \u00b6 The TransIP Provider minimal TTL is used when the TTL is 0. The minimal TTL is 60s. Vultr Provider \u00b6 The Vultr provider minimal TTL is used when the TTL is 0. The default is 1 hour. UltraDNS \u00b6 The UltraDNS provider minimal TTL is used when the TTL is not provided. The default TTL is account level default TTL, if defined, otherwise 24 hours.","title":"TTL"},{"location":"ttl/#configure-dns-record-ttl-time-to-live","text":"An optional annotation external-dns.alpha.kubernetes.io/ttl is available to customize the TTL value of a DNS record. TTL is specified as an integer encoded as string representing seconds. To configure it, simply annotate a service/ingress, e.g.: apiVersion : v1 kind : Service metadata : annotations : external-dns.alpha.kubernetes.io/hostname : nginx.external-dns-test.my-org.com. external-dns.alpha.kubernetes.io/ttl : \"60\" ... TTL can also be specified as a duration value parsable by Golang time.ParseDuration : apiVersion : v1 kind : Service metadata : annotations : external-dns.alpha.kubernetes.io/hostname : nginx.external-dns-test.my-org.com. external-dns.alpha.kubernetes.io/ttl : \"1m\" ... Both examples result in the same value of 60 seconds TTL. TTL must be a positive value.","title":"Configure DNS record TTL (Time-To-Live)"},{"location":"ttl/#providers","text":"AWS (Route53) Azure Cloudflare DigitalOcean DNSimple Google InMemory Linode TransIP RFC2136 Vultr UltraDNS PRs welcome!","title":"Providers"},{"location":"ttl/#notes","text":"When the external-dns.alpha.kubernetes.io/ttl annotation is not provided, the TTL will default to 0 seconds and endpoint.TTL.isConfigured() will be false.","title":"Notes"},{"location":"ttl/#aws-provider","text":"The AWS Provider overrides the value to 300s when the TTL is 0. This value is a constant in the provider code.","title":"AWS Provider"},{"location":"ttl/#azure","text":"TTL value should be between 1 and 2,147,483,647 seconds. By default it will be 300s.","title":"Azure"},{"location":"ttl/#cloudflare-provider","text":"CloudFlare overrides the value to \u201cauto\u201d when the TTL is 0.","title":"CloudFlare Provider"},{"location":"ttl/#digitalocean-provider","text":"The DigitalOcean Provider overrides the value to 300s when the TTL is 0. This value is a constant in the provider code.","title":"DigitalOcean Provider"},{"location":"ttl/#dnsimple-provider","text":"The DNSimple Provider default TTL is used when the TTL is 0. The default TTL is 3600s.","title":"DNSimple Provider"},{"location":"ttl/#google-provider","text":"Previously with the Google Provider, TTL\u2019s were hard-coded to 300s. For safety, the Google Provider overrides the value to 300s when the TTL is 0. This value is a constant in the provider code. For the moment, it is impossible to use a TTL value of 0 with the AWS, DigitalOcean, or Google Providers. This behavior may change in the future.","title":"Google Provider"},{"location":"ttl/#linode-provider","text":"The Linode Provider default TTL is used when the TTL is 0. The default is 24 hours","title":"Linode Provider"},{"location":"ttl/#transip-provider","text":"The TransIP Provider minimal TTL is used when the TTL is 0. The minimal TTL is 60s.","title":"TransIP Provider"},{"location":"ttl/#vultr-provider","text":"The Vultr provider minimal TTL is used when the TTL is 0. The default is 1 hour.","title":"Vultr Provider"},{"location":"ttl/#ultradns","text":"The UltraDNS provider minimal TTL is used when the TTL is not provided. The default TTL is account level default TTL, if defined, otherwise 24 hours.","title":"UltraDNS"},{"location":"annotations/annotations/","text":"Annotations \u00b6 ExternalDNS sources support a number of annotations on the Kubernetes resources that they examine. The following table documents which sources support which annotations: Source controller hostname internal-hostname target ttl (provider-specific) Ambassador Yes Connector Contour Yes Yes 1 Yes Yes Yes CloudFoundry CRD F5 Yes Gateway Yes Yes 1 Yes Yes Gloo Yes Yes Ingress Yes Yes 1 Yes Yes Yes Istio Yes Yes 1 Yes Yes Yes Kong Yes Yes Yes Node Yes Yes OpenShift Yes Yes 1 Yes Yes Yes Pod Yes Yes Service Yes Yes 1 Yes 1 2 Yes 3 Yes Yes Skipper Yes Yes 1 Yes Yes Yes Traefik Yes Yes Yes Yes external-dns.alpha.kubernetes.io/access \u00b6 Specifies which set of node IP addresses to use for a Service of type NodePort . If the value is public , use the Nodes\u2019 addresses of type ExternalIP , plus IPv6 addresses of type InternalIP . If the value is private , use the Nodes\u2019 addresses of type InternalIP . If the annotation is not present and there is at least one address of type ExternalIP , behave as if the value were public , otherwise behave as if the value were private . external-dns.alpha.kubernetes.io/controller \u00b6 If this annotation exists and has a value other than dns-controller then the source ignores the resource. external-dns.alpha.kubernetes.io/endpoints-type \u00b6 Specifies which set of addresses to use for a headless Service . If the value is NodeExternalIP , use each relevant Pod \u2019s Node \u2019s address of type ExternalIP plus each IPv6 address of type InternalIP . Otherwise, if the value is HostIP or the --publish-host-ip flag is specified, use each relevant Pod \u2019s Status.HostIP . Otherwise, use the IP of each of the Service \u2019s Endpoints \u2019s Addresses . external-dns.alpha.kubernetes.io/hostname \u00b6 Specifies the domain for the resource\u2019s DNS records. external-dns.alpha.kubernetes.io/ingress-hostname-source \u00b6 Specifies where to get the domain for an Ingress resource. If the value is defined-hosts-only , use only the domains from the Ingress spec. If the value is annotation-only , use only the domains from the Ingress annotations. If the annotation is not present, use the domains from both the spec and annotations. external-dns.alpha.kubernetes.io/internal-hostname \u00b6 Specifies the domain for the resource\u2019s DNS records that are for use from internal networks. For Services of type LoadBalancer , uses the Service \u2019s ClusterIP . For Pods , uses the Pod \u2019s Status.PodIP . external-dns.alpha.kubernetes.io/target \u00b6 Specifies a comma-separated list of values to override the resource\u2019s DNS record targets (RDATA). Targets that parse as IPv4 addresses are published as A records and targets that parse as IPv6 addresses are published as AAAA records. All other targets are published as CNAME records. external-dns.alpha.kubernetes.io/ttl \u00b6 Specifies the TTL (time to live) for the resource\u2019s DNS records. The value may be specified as either a duration or an integer number of seconds. It must be between 1 and 2,147,483,647 seconds. Provider-specific annotations \u00b6 Some providers define their own annotations. Cloud-specific annotations have keys prefixed as follows: Cloud Annotation prefix AWS external-dns.alpha.kubernetes.io/aws- CloudFlare external-dns.alpha.kubernetes.io/cloudflare- IBM Cloud external-dns.alpha.kubernetes.io/ibmcloud- Scaleway external-dns.alpha.kubernetes.io/scw- Additional annotations that are currently implemented only by AWS are: external-dns.alpha.kubernetes.io/alias \u00b6 If the value of this annotation is true , specifies that CNAME records generated by the resource should instead be alias records. This annotation is only relevant if the --aws-prefer-cname flag is specified. external-dns.alpha.kubernetes.io/set-identifier \u00b6 Specifies the set identifier for DNS records generated by the resource. A set identifier differentiates among multiple DNS record sets that have the same combination of domain and type. Which record set or sets are returned to queries is then determined by the configured routing policy. Unless the --ignore-hostname-annotation flag is specified. \u21a9 \u21a9 \u21a9 \u21a9 \u21a9 \u21a9 \u21a9 \u21a9 Only behaves differently than hostname for Service s of type LoadBalancer . \u21a9 Also supported on Pods referenced from a headless Service \u2019s Endpoints . \u21a9","title":"About"},{"location":"annotations/annotations/#annotations","text":"ExternalDNS sources support a number of annotations on the Kubernetes resources that they examine. The following table documents which sources support which annotations: Source controller hostname internal-hostname target ttl (provider-specific) Ambassador Yes Connector Contour Yes Yes 1 Yes Yes Yes CloudFoundry CRD F5 Yes Gateway Yes Yes 1 Yes Yes Gloo Yes Yes Ingress Yes Yes 1 Yes Yes Yes Istio Yes Yes 1 Yes Yes Yes Kong Yes Yes Yes Node Yes Yes OpenShift Yes Yes 1 Yes Yes Yes Pod Yes Yes Service Yes Yes 1 Yes 1 2 Yes 3 Yes Yes Skipper Yes Yes 1 Yes Yes Yes Traefik Yes Yes Yes Yes","title":"Annotations"},{"location":"annotations/annotations/#external-dnsalphakubernetesioaccess","text":"Specifies which set of node IP addresses to use for a Service of type NodePort . If the value is public , use the Nodes\u2019 addresses of type ExternalIP , plus IPv6 addresses of type InternalIP . If the value is private , use the Nodes\u2019 addresses of type InternalIP . If the annotation is not present and there is at least one address of type ExternalIP , behave as if the value were public , otherwise behave as if the value were private .","title":"external-dns.alpha.kubernetes.io/access"},{"location":"annotations/annotations/#external-dnsalphakubernetesiocontroller","text":"If this annotation exists and has a value other than dns-controller then the source ignores the resource.","title":"external-dns.alpha.kubernetes.io/controller"},{"location":"annotations/annotations/#external-dnsalphakubernetesioendpoints-type","text":"Specifies which set of addresses to use for a headless Service . If the value is NodeExternalIP , use each relevant Pod \u2019s Node \u2019s address of type ExternalIP plus each IPv6 address of type InternalIP . Otherwise, if the value is HostIP or the --publish-host-ip flag is specified, use each relevant Pod \u2019s Status.HostIP . Otherwise, use the IP of each of the Service \u2019s Endpoints \u2019s Addresses .","title":"external-dns.alpha.kubernetes.io/endpoints-type"},{"location":"annotations/annotations/#external-dnsalphakubernetesiohostname","text":"Specifies the domain for the resource\u2019s DNS records.","title":"external-dns.alpha.kubernetes.io/hostname"},{"location":"annotations/annotations/#external-dnsalphakubernetesioingress-hostname-source","text":"Specifies where to get the domain for an Ingress resource. If the value is defined-hosts-only , use only the domains from the Ingress spec. If the value is annotation-only , use only the domains from the Ingress annotations. If the annotation is not present, use the domains from both the spec and annotations.","title":"external-dns.alpha.kubernetes.io/ingress-hostname-source"},{"location":"annotations/annotations/#external-dnsalphakubernetesiointernal-hostname","text":"Specifies the domain for the resource\u2019s DNS records that are for use from internal networks. For Services of type LoadBalancer , uses the Service \u2019s ClusterIP . For Pods , uses the Pod \u2019s Status.PodIP .","title":"external-dns.alpha.kubernetes.io/internal-hostname"},{"location":"annotations/annotations/#external-dnsalphakubernetesiotarget","text":"Specifies a comma-separated list of values to override the resource\u2019s DNS record targets (RDATA). Targets that parse as IPv4 addresses are published as A records and targets that parse as IPv6 addresses are published as AAAA records. All other targets are published as CNAME records.","title":"external-dns.alpha.kubernetes.io/target"},{"location":"annotations/annotations/#external-dnsalphakubernetesiottl","text":"Specifies the TTL (time to live) for the resource\u2019s DNS records. The value may be specified as either a duration or an integer number of seconds. It must be between 1 and 2,147,483,647 seconds.","title":"external-dns.alpha.kubernetes.io/ttl"},{"location":"annotations/annotations/#provider-specific-annotations","text":"Some providers define their own annotations. Cloud-specific annotations have keys prefixed as follows: Cloud Annotation prefix AWS external-dns.alpha.kubernetes.io/aws- CloudFlare external-dns.alpha.kubernetes.io/cloudflare- IBM Cloud external-dns.alpha.kubernetes.io/ibmcloud- Scaleway external-dns.alpha.kubernetes.io/scw- Additional annotations that are currently implemented only by AWS are:","title":"Provider-specific annotations"},{"location":"annotations/annotations/#external-dnsalphakubernetesioalias","text":"If the value of this annotation is true , specifies that CNAME records generated by the resource should instead be alias records. This annotation is only relevant if the --aws-prefer-cname flag is specified.","title":"external-dns.alpha.kubernetes.io/alias"},{"location":"annotations/annotations/#external-dnsalphakubernetesioset-identifier","text":"Specifies the set identifier for DNS records generated by the resource. A set identifier differentiates among multiple DNS record sets that have the same combination of domain and type. Which record set or sets are returned to queries is then determined by the configured routing policy. Unless the --ignore-hostname-annotation flag is specified. \u21a9 \u21a9 \u21a9 \u21a9 \u21a9 \u21a9 \u21a9 \u21a9 Only behaves differently than hostname for Service s of type LoadBalancer . \u21a9 Also supported on Pods referenced from a headless Service \u2019s Endpoints . \u21a9","title":"external-dns.alpha.kubernetes.io/set-identifier"},{"location":"contributing/chart/","text":"Helm Chart \u00b6 Chart Changes \u00b6 When contributing chart changes please follow the same process as when contributing other content but also please DON\u2019T modify Chart.yaml in the PR as this would result in a chart release when merged and will mean that your PR will need modifying before it can be accepted. The chart version will be updated as part of the PR to release the chart. Please DO add your changes to the CHANGELOG.md file in the chart directory under the ## [UNRELEASED] section, if there isn\u2019t an uncommented ## [UNRELEASED] section please copy the commented out template and use that.","title":"Helm Chart"},{"location":"contributing/chart/#helm-chart","text":"","title":"Helm Chart"},{"location":"contributing/chart/#chart-changes","text":"When contributing chart changes please follow the same process as when contributing other content but also please DON\u2019T modify Chart.yaml in the PR as this would result in a chart release when merged and will mean that your PR will need modifying before it can be accepted. The chart version will be updated as part of the PR to release the chart. Please DO add your changes to the CHANGELOG.md file in the chart directory under the ## [UNRELEASED] section, if there isn\u2019t an uncommented ## [UNRELEASED] section please copy the commented out template and use that.","title":"Chart Changes"},{"location":"contributing/crd-source/","text":"CRD Source \u00b6 CRD source provides a generic mechanism to manage DNS records in your favourite DNS provider supported by external-dns. Details \u00b6 CRD source watches for a user specified CRD to extract Endpoints from its Spec . So users need to create such a CRD and register it to the kubernetes cluster and then create new object(s) of the CRD specifying the Endpoints. Registering CRD \u00b6 Here is typical example of CRD API type which provides Endpoints to CRD source : type TTL int64 type Targets [] string type ProviderSpecificProperty struct { Name string `json:\"name,omitempty\"` Value string `json:\"value,omitempty\"` } type ProviderSpecific [] ProviderSpecificProperty type Labels map [ string ] string type Endpoint struct { // The hostname of the DNS record DNSName string `json:\"dnsName,omitempty\"` // The targets the DNS record points to Targets Targets `json:\"targets,omitempty\"` // RecordType type of record, e.g. CNAME, A, SRV, TXT etc RecordType string `json:\"recordType,omitempty\"` // TTL for the record RecordTTL TTL `json:\"recordTTL,omitempty\"` // Labels stores labels defined for the Endpoint // +optional Labels Labels `json:\"labels,omitempty\"` // ProviderSpecific stores provider specific config // +optional ProviderSpecific ProviderSpecific `json:\"providerSpecific,omitempty\"` } type DNSEndpointSpec struct { Endpoints [] * Endpoint `json:\"endpoints,omitempty\"` } type DNSEndpointStatus struct { // The generation observed by the external-dns controller. // +optional ObservedGeneration int64 `json:\"observedGeneration,omitempty\"` } // +genclient // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // DNSEndpoint is the CRD wrapper for Endpoint // +k8s:openapi-gen=true // +kubebuilder:resource:path=dnsendpoints // +kubebuilder:subresource:status type DNSEndpoint struct { metav1 . TypeMeta `json:\",inline\"` metav1 . ObjectMeta `json:\"metadata,omitempty\"` Spec DNSEndpointSpec `json:\"spec,omitempty\"` Status DNSEndpointStatus `json:\"status,omitempty\"` } Refer to kubebuilder to create and register the CRD. Usage \u00b6 One can use CRD source by specifying --source flag with crd and specifying the ApiVersion and Kind of the CRD with --crd-source-apiversion and crd-source-kind respectively. for e.g: $ build/external-dns --source crd --crd-source-apiversion externaldns.k8s.io/v1alpha1 --crd-source-kind DNSEndpoint --provider inmemory --once --dry-run Creating DNS Records \u00b6 Create the objects of CRD type by filling in the fields of CRD and DNS record would be created accordingly. Example \u00b6 Here is an example CRD manifest generated by kubebuilder. Apply this to register the CRD $ kubectl apply --validate=false -f docs/contributing/crd-source/crd-manifest.yaml customresourcedefinition.apiextensions.k8s.io \"dnsendpoints.externaldns.k8s.io\" created Then you can create the dns-endpoint yaml similar to dnsendpoint-example $ kubectl apply -f docs/contributing/crd-source/dnsendpoint-example.yaml dnsendpoint.externaldns.k8s.io \"examplednsrecord\" created Run external-dns in dry-mode to see whether external-dns picks up the DNS record from CRD. $ build/external-dns --source crd --crd-source-apiversion externaldns.k8s.io/v1alpha1 --crd-source-kind DNSEndpoint --provider inmemory --once --dry-run INFO[0000] running in dry-run mode. No changes to DNS records will be made. INFO[0000] Connected to cluster at https://192.168.99.100:8443 INFO[0000] CREATE: foo.bar.com 180 IN A 192.168.99.216 INFO[0000] CREATE: foo.bar.com 0 IN TXT \"heritage=external-dns,external-dns/owner=default\" RBAC configuration \u00b6 If you use RBAC, extend the external-dns ClusterRole with: - apiGroups: [\"externaldns.k8s.io\"] resources: [\"dnsendpoints\"] verbs: [\"get\",\"watch\",\"list\"] - apiGroups: [\"externaldns.k8s.io\"] resources: [\"dnsendpoints/status\"] verbs: [\"*\"]","title":"CRD Source"},{"location":"contributing/crd-source/#crd-source","text":"CRD source provides a generic mechanism to manage DNS records in your favourite DNS provider supported by external-dns.","title":"CRD Source"},{"location":"contributing/crd-source/#details","text":"CRD source watches for a user specified CRD to extract Endpoints from its Spec . So users need to create such a CRD and register it to the kubernetes cluster and then create new object(s) of the CRD specifying the Endpoints.","title":"Details"},{"location":"contributing/crd-source/#registering-crd","text":"Here is typical example of CRD API type which provides Endpoints to CRD source : type TTL int64 type Targets [] string type ProviderSpecificProperty struct { Name string `json:\"name,omitempty\"` Value string `json:\"value,omitempty\"` } type ProviderSpecific [] ProviderSpecificProperty type Labels map [ string ] string type Endpoint struct { // The hostname of the DNS record DNSName string `json:\"dnsName,omitempty\"` // The targets the DNS record points to Targets Targets `json:\"targets,omitempty\"` // RecordType type of record, e.g. CNAME, A, SRV, TXT etc RecordType string `json:\"recordType,omitempty\"` // TTL for the record RecordTTL TTL `json:\"recordTTL,omitempty\"` // Labels stores labels defined for the Endpoint // +optional Labels Labels `json:\"labels,omitempty\"` // ProviderSpecific stores provider specific config // +optional ProviderSpecific ProviderSpecific `json:\"providerSpecific,omitempty\"` } type DNSEndpointSpec struct { Endpoints [] * Endpoint `json:\"endpoints,omitempty\"` } type DNSEndpointStatus struct { // The generation observed by the external-dns controller. // +optional ObservedGeneration int64 `json:\"observedGeneration,omitempty\"` } // +genclient // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // DNSEndpoint is the CRD wrapper for Endpoint // +k8s:openapi-gen=true // +kubebuilder:resource:path=dnsendpoints // +kubebuilder:subresource:status type DNSEndpoint struct { metav1 . TypeMeta `json:\",inline\"` metav1 . ObjectMeta `json:\"metadata,omitempty\"` Spec DNSEndpointSpec `json:\"spec,omitempty\"` Status DNSEndpointStatus `json:\"status,omitempty\"` } Refer to kubebuilder to create and register the CRD.","title":"Registering CRD"},{"location":"contributing/crd-source/#usage","text":"One can use CRD source by specifying --source flag with crd and specifying the ApiVersion and Kind of the CRD with --crd-source-apiversion and crd-source-kind respectively. for e.g: $ build/external-dns --source crd --crd-source-apiversion externaldns.k8s.io/v1alpha1 --crd-source-kind DNSEndpoint --provider inmemory --once --dry-run","title":"Usage"},{"location":"contributing/crd-source/#creating-dns-records","text":"Create the objects of CRD type by filling in the fields of CRD and DNS record would be created accordingly.","title":"Creating DNS Records"},{"location":"contributing/crd-source/#example","text":"Here is an example CRD manifest generated by kubebuilder. Apply this to register the CRD $ kubectl apply --validate=false -f docs/contributing/crd-source/crd-manifest.yaml customresourcedefinition.apiextensions.k8s.io \"dnsendpoints.externaldns.k8s.io\" created Then you can create the dns-endpoint yaml similar to dnsendpoint-example $ kubectl apply -f docs/contributing/crd-source/dnsendpoint-example.yaml dnsendpoint.externaldns.k8s.io \"examplednsrecord\" created Run external-dns in dry-mode to see whether external-dns picks up the DNS record from CRD. $ build/external-dns --source crd --crd-source-apiversion externaldns.k8s.io/v1alpha1 --crd-source-kind DNSEndpoint --provider inmemory --once --dry-run INFO[0000] running in dry-run mode. No changes to DNS records will be made. INFO[0000] Connected to cluster at https://192.168.99.100:8443 INFO[0000] CREATE: foo.bar.com 180 IN A 192.168.99.216 INFO[0000] CREATE: foo.bar.com 0 IN TXT \"heritage=external-dns,external-dns/owner=default\"","title":"Example"},{"location":"contributing/crd-source/#rbac-configuration","text":"If you use RBAC, extend the external-dns ClusterRole with: - apiGroups: [\"externaldns.k8s.io\"] resources: [\"dnsendpoints\"] verbs: [\"get\",\"watch\",\"list\"] - apiGroups: [\"externaldns.k8s.io\"] resources: [\"dnsendpoints/status\"] verbs: [\"*\"]","title":"RBAC configuration"},{"location":"contributing/getting-started/","text":"Quick Start \u00b6 Git Go 1.20+ Go modules golangci-lint ko kubectl Compile and run locally against a remote k8s cluster. git clone https://github.com/kubernetes-sigs/external-dns.git && cd external-dns make build # login to remote k8s cluster ./build/external-dns --source = service --provider = inmemory --once Run linting, unit tests, and coverage report. make lint make test make cover-html Build container image. make build.push IMAGE = your-registry/external-dns Design \u00b6 ExternalDNS\u2019s sources of DNS records live in package source . They implement the Source interface that has a single method Endpoints which returns the represented source\u2019s objects converted to Endpoints . Endpoints are just a tuple of DNS name and target where target can be an IP or another hostname. For example, the ServiceSource returns all Services converted to Endpoints where the hostname is the value of the external-dns.alpha.kubernetes.io/hostname annotation and the target is the IP of the load balancer or where the hostname is the value of the external-dns.alpha.kubernetes.io/internal-hostname annotation and the target is the IP of the service ClusterIP. This list of endpoints is passed to the Plan which determines the difference between the current DNS records and the desired list of Endpoints . Once the difference has been figured out the list of intended changes is passed to a Registry which live in the registry package. The registry is a wrapper and access point to DNS provider. Registry implements the ownership concept by marking owned records and filtering out records not owned by ExternalDNS before passing them to DNS provider. The provider is the adapter to the DNS provider, e.g. Google Cloud DNS. It implements two methods: ApplyChanges to apply a set of changes filtered by Registry and Records to retrieve the current list of records from the DNS provider. The orchestration between the different components is controlled by the controller . You can pick which Source and Provider to use at runtime via the --source and --provider flags, respectively. Adding a DNS Provider \u00b6 A typical way to start on, e.g. a CoreDNS provider, would be to add a coredns.go to the providers package and implement the interface methods. Then you would have to register your provider under a name in main.go , e.g. coredns , and would be able to trigger it\u2019s functions via setting --provider=coredns . Note, how your provider doesn\u2019t need to know anything about where the DNS records come from, nor does it have to figure out the difference between the current and the desired state, it merely executes the actions calculated by the plan. Running GitHub Actions locally \u00b6 You can also extend the CI workflow which is currently implemented as GitHub Action within the workflow folder. In order to test your changes before committing you can leverage act to run the GitHub Action locally. Follow the installation instructions in the nektos/act README.md . Afterwards just run act within the root folder of the project. For further usage of act refer to its documentation.","title":"Quick Start"},{"location":"contributing/getting-started/#quick-start","text":"Git Go 1.20+ Go modules golangci-lint ko kubectl Compile and run locally against a remote k8s cluster. git clone https://github.com/kubernetes-sigs/external-dns.git && cd external-dns make build # login to remote k8s cluster ./build/external-dns --source = service --provider = inmemory --once Run linting, unit tests, and coverage report. make lint make test make cover-html Build container image. make build.push IMAGE = your-registry/external-dns","title":"Quick Start"},{"location":"contributing/getting-started/#design","text":"ExternalDNS\u2019s sources of DNS records live in package source . They implement the Source interface that has a single method Endpoints which returns the represented source\u2019s objects converted to Endpoints . Endpoints are just a tuple of DNS name and target where target can be an IP or another hostname. For example, the ServiceSource returns all Services converted to Endpoints where the hostname is the value of the external-dns.alpha.kubernetes.io/hostname annotation and the target is the IP of the load balancer or where the hostname is the value of the external-dns.alpha.kubernetes.io/internal-hostname annotation and the target is the IP of the service ClusterIP. This list of endpoints is passed to the Plan which determines the difference between the current DNS records and the desired list of Endpoints . Once the difference has been figured out the list of intended changes is passed to a Registry which live in the registry package. The registry is a wrapper and access point to DNS provider. Registry implements the ownership concept by marking owned records and filtering out records not owned by ExternalDNS before passing them to DNS provider. The provider is the adapter to the DNS provider, e.g. Google Cloud DNS. It implements two methods: ApplyChanges to apply a set of changes filtered by Registry and Records to retrieve the current list of records from the DNS provider. The orchestration between the different components is controlled by the controller . You can pick which Source and Provider to use at runtime via the --source and --provider flags, respectively.","title":"Design"},{"location":"contributing/getting-started/#adding-a-dns-provider","text":"A typical way to start on, e.g. a CoreDNS provider, would be to add a coredns.go to the providers package and implement the interface methods. Then you would have to register your provider under a name in main.go , e.g. coredns , and would be able to trigger it\u2019s functions via setting --provider=coredns . Note, how your provider doesn\u2019t need to know anything about where the DNS records come from, nor does it have to figure out the difference between the current and the desired state, it merely executes the actions calculated by the plan.","title":"Adding a DNS Provider"},{"location":"contributing/getting-started/#running-github-actions-locally","text":"You can also extend the CI workflow which is currently implemented as GitHub Action within the workflow folder. In order to test your changes before committing you can leverage act to run the GitHub Action locally. Follow the installation instructions in the nektos/act README.md . Afterwards just run act within the root folder of the project. For further usage of act refer to its documentation.","title":"Running GitHub Actions locally"},{"location":"contributing/sources-and-providers/","text":"Sources and Providers \u00b6 ExternalDNS supports swapping out endpoint sources and DNS providers and both sides are pluggable. There currently exist three sources and four provider implementations. Sources \u00b6 Sources are an abstraction over any kind of source of desired Endpoints, e.g.: * a list of Service objects from Kubernetes * a random list for testing purposes * an aggregated list of multiple nested sources The Source interface has a single method called Endpoints that should return all desired Endpoint objects as a flat list. type Source interface { Endpoints () ([] * endpoint . Endpoint , error ) } All sources live in package source . ServiceSource : collects all Services that have an external IP and returns them as Endpoint objects. The desired DNS name corresponds to an annotation set on the Service or is compiled from the Service attributes via the FQDN Go template string. IngressSource : collects all Ingresses that have an external IP and returns them as Endpoint objects. The desired DNS name corresponds to the host rules defined in the Ingress object. IstioGatewaySource : collects all Istio Gateways and returns them as Endpoint objects. The desired DNS name corresponds to the hosts listed within the servers spec of each Gateway object. ContourIngressRouteSource : collects all Contour IngressRoutes and returns them as Endpoint objects. The desired DNS name corresponds to the virtualhost.fqdn listed within the spec of each IngressRoute object. FakeSource : returns a random list of Endpoints for the purpose of testing providers without having access to a Kubernetes cluster. ConnectorSource : returns a list of Endpoint objects which are served by a tcp server configured through connector-source-server flag. CRDSource : returns a list of Endpoint objects sourced from the spec of CRD objects. For more details refer to CRD source documentation. EmptySource : returns an empty list of Endpoint objects for the purpose of testing and cleaning out entries. Providers \u00b6 Providers are an abstraction over any kind of sink for desired Endpoints, e.g.: * storing them in Google Cloud DNS * printing them to stdout for testing purposes * fanning out to multiple nested providers The Provider interface has two methods: Records and ApplyChanges . Records should return all currently existing DNS records converted to Endpoint objects as a flat list. Upon receiving a change set (via an object of plan.Changes ), ApplyChanges should translate these to the provider specific actions in order to persist them in the provider\u2019s storage. type Provider interface { Records () ([] * endpoint . Endpoint , error ) ApplyChanges ( changes * plan . Changes ) error } The interface tries to be generic and assumes a flat list of records for both functions. However, many providers scope records into zones. Therefore, the provider implementation has to do some extra work to return that flat list. For instance, the AWS provider fetches the list of all hosted zones before it can return or apply the list of records. If the provider has no concept of zones or if it makes sense to cache the list of hosted zones it is happily allowed to do so. Furthermore, the provider should respect the --domain-filter flag to limit the affected records by a domain suffix. For instance, the AWS provider filters out all hosted zones that doesn\u2019t match that domain filter. All providers live in package provider . GoogleProvider : returns and creates DNS records in Google Cloud DNS AWSProvider : returns and creates DNS records in AWS Route 53 AzureProvider : returns and creates DNS records in Azure DNS InMemoryProvider : Keeps a list of records in local memory Usage \u00b6 You can choose any combination of sources and providers on the command line. Given a cluster on AWS you would most likely want to use the Service and Ingress Source in combination with the AWS provider. Service + InMemory is useful for testing your service collecting functionality, whereas Fake + Google is useful for testing that the Google provider behaves correctly, etc.","title":"Sources and Providers"},{"location":"contributing/sources-and-providers/#sources-and-providers","text":"ExternalDNS supports swapping out endpoint sources and DNS providers and both sides are pluggable. There currently exist three sources and four provider implementations.","title":"Sources and Providers"},{"location":"contributing/sources-and-providers/#sources","text":"Sources are an abstraction over any kind of source of desired Endpoints, e.g.: * a list of Service objects from Kubernetes * a random list for testing purposes * an aggregated list of multiple nested sources The Source interface has a single method called Endpoints that should return all desired Endpoint objects as a flat list. type Source interface { Endpoints () ([] * endpoint . Endpoint , error ) } All sources live in package source . ServiceSource : collects all Services that have an external IP and returns them as Endpoint objects. The desired DNS name corresponds to an annotation set on the Service or is compiled from the Service attributes via the FQDN Go template string. IngressSource : collects all Ingresses that have an external IP and returns them as Endpoint objects. The desired DNS name corresponds to the host rules defined in the Ingress object. IstioGatewaySource : collects all Istio Gateways and returns them as Endpoint objects. The desired DNS name corresponds to the hosts listed within the servers spec of each Gateway object. ContourIngressRouteSource : collects all Contour IngressRoutes and returns them as Endpoint objects. The desired DNS name corresponds to the virtualhost.fqdn listed within the spec of each IngressRoute object. FakeSource : returns a random list of Endpoints for the purpose of testing providers without having access to a Kubernetes cluster. ConnectorSource : returns a list of Endpoint objects which are served by a tcp server configured through connector-source-server flag. CRDSource : returns a list of Endpoint objects sourced from the spec of CRD objects. For more details refer to CRD source documentation. EmptySource : returns an empty list of Endpoint objects for the purpose of testing and cleaning out entries.","title":"Sources"},{"location":"contributing/sources-and-providers/#providers","text":"Providers are an abstraction over any kind of sink for desired Endpoints, e.g.: * storing them in Google Cloud DNS * printing them to stdout for testing purposes * fanning out to multiple nested providers The Provider interface has two methods: Records and ApplyChanges . Records should return all currently existing DNS records converted to Endpoint objects as a flat list. Upon receiving a change set (via an object of plan.Changes ), ApplyChanges should translate these to the provider specific actions in order to persist them in the provider\u2019s storage. type Provider interface { Records () ([] * endpoint . Endpoint , error ) ApplyChanges ( changes * plan . Changes ) error } The interface tries to be generic and assumes a flat list of records for both functions. However, many providers scope records into zones. Therefore, the provider implementation has to do some extra work to return that flat list. For instance, the AWS provider fetches the list of all hosted zones before it can return or apply the list of records. If the provider has no concept of zones or if it makes sense to cache the list of hosted zones it is happily allowed to do so. Furthermore, the provider should respect the --domain-filter flag to limit the affected records by a domain suffix. For instance, the AWS provider filters out all hosted zones that doesn\u2019t match that domain filter. All providers live in package provider . GoogleProvider : returns and creates DNS records in Google Cloud DNS AWSProvider : returns and creates DNS records in AWS Route 53 AzureProvider : returns and creates DNS records in Azure DNS InMemoryProvider : Keeps a list of records in local memory","title":"Providers"},{"location":"contributing/sources-and-providers/#usage","text":"You can choose any combination of sources and providers on the command line. Given a cluster on AWS you would most likely want to use the Service and Ingress Source in combination with the AWS provider. Service + InMemory is useful for testing your service collecting functionality, whereas Fake + Google is useful for testing that the Google provider behaves correctly, etc.","title":"Usage"},{"location":"proposal/multi-target/","text":"Multiple Targets per hostname \u00b6 (November 2017) Purpose \u00b6 One should be able to define multiple targets (IPs/Hostnames) in the same Kubernetes resource object and expect ExternalDNS create DNS record(s) with a specified hostname and all targets. So far the connection between k8s resources (ingress/services) and DNS records were not streamlined. This proposal aims to make the connection explicit, making k8s resources acquire or release certain DNS names. As long as the resource ingress/service owns the record it can have multiple targets enable iff they are specified in the same resource. Use cases \u00b6 See https://github.com/kubernetes-sigs/external-dns/issues/239 Current behaviour \u00b6 (as of the moment of writing) Central piece of enabling multi-target is having consistent and correct behaviour in plan component in regards to how endpoints generated from kubernetes resources are mapped to dns records. Current implementation of the plan has inconsistent behaviour in the following scenarios, all of which must be resolved before multi-target support can be enabled in the provider implementations: No records registered so far. Two different ingresses request same hostname but different targets, e.g. Ingress A: example.com -> 1.1.1.1 and Ingress B: example.com -> 2.2.2.2 Current Behaviour : both are added to the \u201cCreate\u201d (records to be created) list and passed to Provider Expected Behaviour : only one (random/ or according to predefined strategy) should be chosen and passed to Provider NOTE : while this seems to go against multi-target support, this is done so no other resource can \u201chijack\u201d already created DNS record. Multi targets are supported only on per single resource basis Now let\u2019s say Ingress A was chosen and successfully created, but both ingress A and B are still there. So on next iteration ExternalDNS would see both again in the Desired list. Current Behaviour : DNS record target will change to that of Ingress B. Expected Behaviour : Ingress A should stay unchanged. Ingress B record is not created DNS record for Ingress A was created but its target has changed. Ingress B is still there Current Behaviour : Undetermined behaviour based on which ingress will be parsed last. Expected Behaviour : DNS record should point to the new target specified in A. Ingress B should still be ignored. NOTE : both 2. and 3. can be resolved if External DNS is aware which resource has already acquired DNS record Ingress C has multiple targets: 1.1.1.1 and 2.2.2.2 Current Behaviour : Both targets are split into different endpoints and we end up in one of the cases above Expected Behaviour : Endpoint should contain list of targets and treated as one ingress object. Requirements and assumptions \u00b6 For this feature to work we have to make sure that: DNS records are now owned by certain ingress/service resources. For External DNS it would mean that TXT records now should store back-reference for the resource this record was created for, i.e. \"heritage=external-dns,external-dns/resource=ingress/default/my-ingress-object-name\" DNS records are updated only: If owning resource target list has changed If owning resource record is not found in the desired list (meaning it was deleted), therefore it will now be owned by another record. So its target list will be updated Changes related to other record properties (e.g. TTL) All of the issues described in Current Behaviour sections are resolved Once Create/Update/Delete lists are calculated correctly (this is where conflicts based on requested DNS names are resolved) they are passed to provider , where provider specific implementation will decide how to convert the structures into required formats. If DNS provider does not (or partially) support multi targets then it is up to the provider to make sure that the change list of records passed to the DNS provider API is valid. TODO : explain best strategy. Additionally see https://github.com/kubernetes-sigs/external-dns/issues/258 Implementation plan \u00b6 Brief summary of open PRs and what they are trying to address: PRs \u00b6 https://github.com/kubernetes-sigs/external-dns/pull/243 - first attempt to add support for multiple targets. It is lagging far behind from tip what it does : unfinished attempt to extend Endpoint struct, for it to allow multiple targets (essentially target string -> targets []string ) action : evaluate if rebasing makes sense, or we can just close it. https://github.com/kubernetes-sigs/external-dns/pull/261 - attempt to rework plan to make it work correctly with multiple targets. what it does : attempts to fix issues with plan described in Current Behaviour section above. Included tests reveal the current problem with plan action : rebase on default branch and make necessary changes to satisfy requirements listed in this document including back-reference to owning record https://github.com/kubernetes-sigs/external-dns/pull/326 - attempt to add multiple target support. what it does : for each pair DNS Name + Record Type it aggregates all targets from the cluster and passes them to Provider. It adds basic support for DO, Azure, Cloudflare, AWS, GCP, however those are not tested (?). (DNSSimple and Infoblox providers were not updated) action : the plan logic will probably needs to be reworked, however the rest concerning support in Providers and extending Endpoint struct can be reused. Rebase on default branch and add missing pieces. Depends on 2 . Related PRs: https://github.com/kubernetes-sigs/external-dns/pull/331/files, https://github.com/kubernetes-sigs/external-dns/pull/347/files - aiming at AWS Route53 weighted records. These PRs should be considered after common agreement about the way to address multi-target support is achieved. Related discussion: https://github.com/kubernetes-sigs/external-dns/issues/196 How to proceed from here \u00b6 The following steps are needed: 1. Make sure consensus regarding the approach is achieved via collaboration on the current document 2. Notify all PR (see above) authors about the agreed approach 3. Implementation: a. `Plan` is working as expected - either based on #261 above or from scratch. `Plan` should be working correctly regardless of multi-target support b. Extensive testing making sure new `plan` does not introduce any breaking changes c. Change Endpoint struct to support multiple targets - based on #326 - integrate it with new `plan` @sethpollack d. Make sure new endpoint format can still be used in providers which have only partial support for multi targets ~~**TODO**: how ?~~ . This is to be done by simply using first target in the targets list. e. Add support for multi target which are already addressed in #326. It goes alongside c. and can be based on the same PR @sethpollack. New providers added since then should maintain same functionality. Extensive testing on all providers before making new release Update all related documentation and explain how multi targets are supported on per provider basis Think of introducing weighted records (see PRs section above) and making them configurable. Open questions \u00b6 Handling cases when ingress/service targets include both hostnames and IPs - postpone this until use cases occurs \u201cWeighted records scope\u201d: https://github.com/kubernetes-sigs/external-dns/issues/196 - this should be considered once multi-target support is implemented","title":"Multiple Targets per hostname"},{"location":"proposal/multi-target/#multiple-targets-per-hostname","text":"(November 2017)","title":"Multiple Targets per hostname"},{"location":"proposal/multi-target/#purpose","text":"One should be able to define multiple targets (IPs/Hostnames) in the same Kubernetes resource object and expect ExternalDNS create DNS record(s) with a specified hostname and all targets. So far the connection between k8s resources (ingress/services) and DNS records were not streamlined. This proposal aims to make the connection explicit, making k8s resources acquire or release certain DNS names. As long as the resource ingress/service owns the record it can have multiple targets enable iff they are specified in the same resource.","title":"Purpose"},{"location":"proposal/multi-target/#use-cases","text":"See https://github.com/kubernetes-sigs/external-dns/issues/239","title":"Use cases"},{"location":"proposal/multi-target/#current-behaviour","text":"(as of the moment of writing) Central piece of enabling multi-target is having consistent and correct behaviour in plan component in regards to how endpoints generated from kubernetes resources are mapped to dns records. Current implementation of the plan has inconsistent behaviour in the following scenarios, all of which must be resolved before multi-target support can be enabled in the provider implementations: No records registered so far. Two different ingresses request same hostname but different targets, e.g. Ingress A: example.com -> 1.1.1.1 and Ingress B: example.com -> 2.2.2.2 Current Behaviour : both are added to the \u201cCreate\u201d (records to be created) list and passed to Provider Expected Behaviour : only one (random/ or according to predefined strategy) should be chosen and passed to Provider NOTE : while this seems to go against multi-target support, this is done so no other resource can \u201chijack\u201d already created DNS record. Multi targets are supported only on per single resource basis Now let\u2019s say Ingress A was chosen and successfully created, but both ingress A and B are still there. So on next iteration ExternalDNS would see both again in the Desired list. Current Behaviour : DNS record target will change to that of Ingress B. Expected Behaviour : Ingress A should stay unchanged. Ingress B record is not created DNS record for Ingress A was created but its target has changed. Ingress B is still there Current Behaviour : Undetermined behaviour based on which ingress will be parsed last. Expected Behaviour : DNS record should point to the new target specified in A. Ingress B should still be ignored. NOTE : both 2. and 3. can be resolved if External DNS is aware which resource has already acquired DNS record Ingress C has multiple targets: 1.1.1.1 and 2.2.2.2 Current Behaviour : Both targets are split into different endpoints and we end up in one of the cases above Expected Behaviour : Endpoint should contain list of targets and treated as one ingress object.","title":"Current behaviour"},{"location":"proposal/multi-target/#requirements-and-assumptions","text":"For this feature to work we have to make sure that: DNS records are now owned by certain ingress/service resources. For External DNS it would mean that TXT records now should store back-reference for the resource this record was created for, i.e. \"heritage=external-dns,external-dns/resource=ingress/default/my-ingress-object-name\" DNS records are updated only: If owning resource target list has changed If owning resource record is not found in the desired list (meaning it was deleted), therefore it will now be owned by another record. So its target list will be updated Changes related to other record properties (e.g. TTL) All of the issues described in Current Behaviour sections are resolved Once Create/Update/Delete lists are calculated correctly (this is where conflicts based on requested DNS names are resolved) they are passed to provider , where provider specific implementation will decide how to convert the structures into required formats. If DNS provider does not (or partially) support multi targets then it is up to the provider to make sure that the change list of records passed to the DNS provider API is valid. TODO : explain best strategy. Additionally see https://github.com/kubernetes-sigs/external-dns/issues/258","title":"Requirements and assumptions"},{"location":"proposal/multi-target/#implementation-plan","text":"Brief summary of open PRs and what they are trying to address:","title":"Implementation plan"},{"location":"proposal/multi-target/#prs","text":"https://github.com/kubernetes-sigs/external-dns/pull/243 - first attempt to add support for multiple targets. It is lagging far behind from tip what it does : unfinished attempt to extend Endpoint struct, for it to allow multiple targets (essentially target string -> targets []string ) action : evaluate if rebasing makes sense, or we can just close it. https://github.com/kubernetes-sigs/external-dns/pull/261 - attempt to rework plan to make it work correctly with multiple targets. what it does : attempts to fix issues with plan described in Current Behaviour section above. Included tests reveal the current problem with plan action : rebase on default branch and make necessary changes to satisfy requirements listed in this document including back-reference to owning record https://github.com/kubernetes-sigs/external-dns/pull/326 - attempt to add multiple target support. what it does : for each pair DNS Name + Record Type it aggregates all targets from the cluster and passes them to Provider. It adds basic support for DO, Azure, Cloudflare, AWS, GCP, however those are not tested (?). (DNSSimple and Infoblox providers were not updated) action : the plan logic will probably needs to be reworked, however the rest concerning support in Providers and extending Endpoint struct can be reused. Rebase on default branch and add missing pieces. Depends on 2 . Related PRs: https://github.com/kubernetes-sigs/external-dns/pull/331/files, https://github.com/kubernetes-sigs/external-dns/pull/347/files - aiming at AWS Route53 weighted records. These PRs should be considered after common agreement about the way to address multi-target support is achieved. Related discussion: https://github.com/kubernetes-sigs/external-dns/issues/196","title":"PRs"},{"location":"proposal/multi-target/#how-to-proceed-from-here","text":"The following steps are needed: 1. Make sure consensus regarding the approach is achieved via collaboration on the current document 2. Notify all PR (see above) authors about the agreed approach 3. Implementation: a. `Plan` is working as expected - either based on #261 above or from scratch. `Plan` should be working correctly regardless of multi-target support b. Extensive testing making sure new `plan` does not introduce any breaking changes c. Change Endpoint struct to support multiple targets - based on #326 - integrate it with new `plan` @sethpollack d. Make sure new endpoint format can still be used in providers which have only partial support for multi targets ~~**TODO**: how ?~~ . This is to be done by simply using first target in the targets list. e. Add support for multi target which are already addressed in #326. It goes alongside c. and can be based on the same PR @sethpollack. New providers added since then should maintain same functionality. Extensive testing on all providers before making new release Update all related documentation and explain how multi targets are supported on per provider basis Think of introducing weighted records (see PRs section above) and making them configurable.","title":"How to proceed from here"},{"location":"proposal/multi-target/#open-questions","text":"Handling cases when ingress/service targets include both hostnames and IPs - postpone this until use cases occurs \u201cWeighted records scope\u201d: https://github.com/kubernetes-sigs/external-dns/issues/196 - this should be considered once multi-target support is implemented","title":"Open questions"},{"location":"registry/dynamodb/","text":"The DynamoDB registry \u00b6 The DynamoDB registry stores DNS record metadata in an AWS DynamoDB table. The DynamoDB Table \u00b6 By default, the DynamoDB registry stores data in the table named external-dns . A different table may be specified using the --dynamodb-table flag. A different region may be specified using the --dynamodb-region flag. The table must have a partition (hash) key named k and string type. The table must not have a sort (range) key. IAM permissions \u00b6 The ExternalDNS Role must be granted the following permissions: { \"Effect\" : \"Allow\" , \"Action\" : [ \"DynamoDB:DescribeTable\" , \"DynamoDB:PartiQLDelete\" , \"DynamoDB:PartiQLInsert\" , \"DynamoDB:PartiQLUpdate\" , \"DynamoDB:Scan\" ], \"Resource\" : [ \"arn:aws:dynamodb:*:*:table/external-dns\" ] } The region and account ID may be specified explicitly specified instead of using wildcards. Caching \u00b6 The DynamoDB registry can optionally cache DNS records read from the provider. This can mitigate rate limits imposed by the provider. Caching is enabled by specifying a cache duration with the --txt-cache-interval flag. Migration from TXT registry \u00b6 If any ownership TXT records exist for the configured owner, the DynamoDB registry will migrate the metadata therein to the DynamoDB table. If any such TXT records exist, any previous values for --txt-prefix , --txt-suffix , --txt-wildcard-replacement , and --txt-encrypt-aes-key must be supplied. If TXT records are in the set of managed record types specified by --managed-record-types , it will then delete the ownership TXT records on a subsequent reconciliation.","title":"DynamoDB"},{"location":"registry/dynamodb/#the-dynamodb-registry","text":"The DynamoDB registry stores DNS record metadata in an AWS DynamoDB table.","title":"The DynamoDB registry"},{"location":"registry/dynamodb/#the-dynamodb-table","text":"By default, the DynamoDB registry stores data in the table named external-dns . A different table may be specified using the --dynamodb-table flag. A different region may be specified using the --dynamodb-region flag. The table must have a partition (hash) key named k and string type. The table must not have a sort (range) key.","title":"The DynamoDB Table"},{"location":"registry/dynamodb/#iam-permissions","text":"The ExternalDNS Role must be granted the following permissions: { \"Effect\" : \"Allow\" , \"Action\" : [ \"DynamoDB:DescribeTable\" , \"DynamoDB:PartiQLDelete\" , \"DynamoDB:PartiQLInsert\" , \"DynamoDB:PartiQLUpdate\" , \"DynamoDB:Scan\" ], \"Resource\" : [ \"arn:aws:dynamodb:*:*:table/external-dns\" ] } The region and account ID may be specified explicitly specified instead of using wildcards.","title":"IAM permissions"},{"location":"registry/dynamodb/#caching","text":"The DynamoDB registry can optionally cache DNS records read from the provider. This can mitigate rate limits imposed by the provider. Caching is enabled by specifying a cache duration with the --txt-cache-interval flag.","title":"Caching"},{"location":"registry/dynamodb/#migration-from-txt-registry","text":"If any ownership TXT records exist for the configured owner, the DynamoDB registry will migrate the metadata therein to the DynamoDB table. If any such TXT records exist, any previous values for --txt-prefix , --txt-suffix , --txt-wildcard-replacement , and --txt-encrypt-aes-key must be supplied. If TXT records are in the set of managed record types specified by --managed-record-types , it will then delete the ownership TXT records on a subsequent reconciliation.","title":"Migration from TXT registry"},{"location":"registry/registry/","text":"Registries \u00b6 A registry persists metadata pertaining to DNS records. The most important metadata is the owning external-dns deployment. This is specified using the --txt-owner-id flag, specifying a value unique to the deployment of external-dns and which doesn\u2019t change for the lifetime of the deployment. Deployments in different clusters but sharing a DNS zone need to use different owner IDs. The registry implementation is specified using the --registry flag. Supported registries \u00b6 txt (default) - Stores metadata in TXT records in the same provider. dynamodb - Stores metadata in an AWS DynamoDB table. noop - Passes metadata directly to the provider. For most providers, this means the metadata is not persisted. aws-sd - Stores metadata in AWS Service Discovery. Only usable with the aws-sd provider.","title":"About"},{"location":"registry/registry/#registries","text":"A registry persists metadata pertaining to DNS records. The most important metadata is the owning external-dns deployment. This is specified using the --txt-owner-id flag, specifying a value unique to the deployment of external-dns and which doesn\u2019t change for the lifetime of the deployment. Deployments in different clusters but sharing a DNS zone need to use different owner IDs. The registry implementation is specified using the --registry flag.","title":"Registries"},{"location":"registry/registry/#supported-registries","text":"txt (default) - Stores metadata in TXT records in the same provider. dynamodb - Stores metadata in an AWS DynamoDB table. noop - Passes metadata directly to the provider. For most providers, this means the metadata is not persisted. aws-sd - Stores metadata in AWS Service Discovery. Only usable with the aws-sd provider.","title":"Supported registries"},{"location":"registry/txt/","text":"The TXT registry \u00b6 The TXT registry is the default registry. It stores DNS record metadata in TXT records, using the same provider. Prefixes and Suffixes \u00b6 In order to avoid having the registry TXT records collide with TXT or CNAME records created from sources, you can configure a fixed prefix or suffix to be added to the first component of the domain of all registry TXT records. The prefix or suffix may not be changed after initial deployment, lest the registry records be orphaned and the metadata be lost. The prefix or suffix may contain the substring %{record_type} , which is replaced with the record type of the DNS record for which it is storing metadata. The prefix is specified using the --txt-prefix flag and the suffix is specified using the --txt-suffix flag. The two flags are mutually exclusive. Wildcard Replacement \u00b6 The --txt-wildcard-replacement flag specifies a string to use to replace the \u201c*\u201d in registry TXT records for wildcard domains. Without using this, registry TXT records for wildcard domains will have invalid domain syntax and be rejected by most providers. Encryption \u00b6 Registry TXT records may contain information, such as the internal ingress name or namespace, considered sensitive, , which attackers could exploit to gather information about your infrastructure. By encrypting TXT records, you can protect this information from unauthorized access. Encryption is enabled by using the --txt-encrypt-enabled flag. The 32-byte AES-256-GCM encryption key must be specified in URL-safe base64 form, using the --txt-encrypt-aes-key flag. Note that the key used for encryption should be a secure key and properly managed to ensure the security of your TXT records. Generating the TXT Encryption Key \u00b6 Python python - c 'import os,base64; print(base64.urlsafe_b64encode(os.urandom(32)).decode())' Bash dd if = /dev/urandom bs = 32 count = 1 2 >/dev/null | base64 | tr -d -- '\\n' | tr -- '+/' '-_' ; echo OpenSSL openssl rand -base64 32 | tr -- '+/' '-_' PowerShell # Add System.Web assembly to session, just in case Add-Type -AssemblyName System . Web [Convert] :: ToBase64String ( [System.Text.Encoding] :: UTF8 . GetBytes ( [System.Web.Security.Membership] :: GeneratePassword ( 32 , 4 ))). Replace ( \"+\" , \"-\" ). Replace ( \"/\" , \"_\" ) Terraform resource \"random_password\" \"txt_key\" { length = 32 override_special = \"-_\" } Manually Encrypting/Decrypting TXT Records \u00b6 In some cases you might need to edit registry TXT records. The following example Go code encrypts and decrypts such records. package main import ( \"fmt\" \"sigs.k8s.io/external-dns/endpoint\" ) func main () { key := [] byte ( \"testtesttesttesttesttesttesttest\" ) encrypted , _ := endpoint . EncryptText ( \"heritage=external-dns,external-dns/owner=example,external-dns/resource=ingress/default/example\" , key , nil , ) decrypted , _ , _ := endpoint . DecryptText ( encrypted , key ) fmt . Println ( decrypted ) } Caching \u00b6 The TXT registry can optionally cache DNS records read from the provider. This can mitigate rate limits imposed by the provider. Caching is enabled by specifying a cache duration with the --txt-cache-interval flag.","title":"TXT"},{"location":"registry/txt/#the-txt-registry","text":"The TXT registry is the default registry. It stores DNS record metadata in TXT records, using the same provider.","title":"The TXT registry"},{"location":"registry/txt/#prefixes-and-suffixes","text":"In order to avoid having the registry TXT records collide with TXT or CNAME records created from sources, you can configure a fixed prefix or suffix to be added to the first component of the domain of all registry TXT records. The prefix or suffix may not be changed after initial deployment, lest the registry records be orphaned and the metadata be lost. The prefix or suffix may contain the substring %{record_type} , which is replaced with the record type of the DNS record for which it is storing metadata. The prefix is specified using the --txt-prefix flag and the suffix is specified using the --txt-suffix flag. The two flags are mutually exclusive.","title":"Prefixes and Suffixes"},{"location":"registry/txt/#wildcard-replacement","text":"The --txt-wildcard-replacement flag specifies a string to use to replace the \u201c*\u201d in registry TXT records for wildcard domains. Without using this, registry TXT records for wildcard domains will have invalid domain syntax and be rejected by most providers.","title":"Wildcard Replacement"},{"location":"registry/txt/#encryption","text":"Registry TXT records may contain information, such as the internal ingress name or namespace, considered sensitive, , which attackers could exploit to gather information about your infrastructure. By encrypting TXT records, you can protect this information from unauthorized access. Encryption is enabled by using the --txt-encrypt-enabled flag. The 32-byte AES-256-GCM encryption key must be specified in URL-safe base64 form, using the --txt-encrypt-aes-key flag. Note that the key used for encryption should be a secure key and properly managed to ensure the security of your TXT records.","title":"Encryption"},{"location":"registry/txt/#generating-the-txt-encryption-key","text":"Python python - c 'import os,base64; print(base64.urlsafe_b64encode(os.urandom(32)).decode())' Bash dd if = /dev/urandom bs = 32 count = 1 2 >/dev/null | base64 | tr -d -- '\\n' | tr -- '+/' '-_' ; echo OpenSSL openssl rand -base64 32 | tr -- '+/' '-_' PowerShell # Add System.Web assembly to session, just in case Add-Type -AssemblyName System . Web [Convert] :: ToBase64String ( [System.Text.Encoding] :: UTF8 . GetBytes ( [System.Web.Security.Membership] :: GeneratePassword ( 32 , 4 ))). Replace ( \"+\" , \"-\" ). Replace ( \"/\" , \"_\" ) Terraform resource \"random_password\" \"txt_key\" { length = 32 override_special = \"-_\" }","title":"Generating the TXT Encryption Key"},{"location":"registry/txt/#manually-encryptingdecrypting-txt-records","text":"In some cases you might need to edit registry TXT records. The following example Go code encrypts and decrypts such records. package main import ( \"fmt\" \"sigs.k8s.io/external-dns/endpoint\" ) func main () { key := [] byte ( \"testtesttesttesttesttesttesttest\" ) encrypted , _ := endpoint . EncryptText ( \"heritage=external-dns,external-dns/owner=example,external-dns/resource=ingress/default/example\" , key , nil , ) decrypted , _ , _ := endpoint . DecryptText ( encrypted , key ) fmt . Println ( decrypted ) }","title":"Manually Encrypting/Decrypting TXT Records"},{"location":"registry/txt/#caching","text":"The TXT registry can optionally cache DNS records read from the provider. This can mitigate rate limits imposed by the provider. Caching is enabled by specifying a cache duration with the --txt-cache-interval flag.","title":"Caching"},{"location":"sources/ingress/","text":"Ingress source \u00b6 The ingress source creates DNS entries based on Ingress.networking.k8s.io resources. Filtering the Ingresses considered \u00b6 The --ingress-class flag filters Ingress resources by a set of ingress classes. The flag may be specified multiple times in order to allow multiple ingress classes. This source supports the --label-filter flag, which filters Ingress resources by a set of labels. Domain names \u00b6 The domain names of the DNS entries created from an Ingress are sourced from the following places: Iterates over the Ingress\u2019s spec.rules , adding any non-empty host . This behavior is suppressed if the --ignore-ingress-rules-spec flag was specified or the Ingress had an external-dns.alpha.kubernetes.io/ingress-hostname-source: annotation-only annotation. Iterates over the Ingress\u2019s spec.tls , adding each member of hosts . This behavior is suppressed if the --ignore-ingress-tls-spec flag was specified or the Ingress had an external-dns.alpha.kubernetes.io/ingress-hostname-source: annotation-only annotation, Adds the hostnames from any external-dns.alpha.kubernetes.io/hostname annotation. This behavior is suppressed if the --ignore-hostname-annotation flag was specified or the Ingress had an external-dns.alpha.kubernetes.io/ingress-hostname-source: defined-hosts-only annotation. If no endpoints were produced for an Ingress by the previous steps or the --combine-fqdn-annotation flag was specified, then adds hostnames generated from any --fqdn-template flag. Targets \u00b6 The targets of the DNS entries created from an Ingress are sourced from the following places: If the Ingress has an external-dns.alpha.kubernetes.io/target annotation, uses the values from that. Otherwise, iterates over the Ingress\u2019s status.loadBalancer.ingress , adding each non-empty ip and hostname .","title":"Ingress"},{"location":"sources/ingress/#ingress-source","text":"The ingress source creates DNS entries based on Ingress.networking.k8s.io resources.","title":"Ingress source"},{"location":"sources/ingress/#filtering-the-ingresses-considered","text":"The --ingress-class flag filters Ingress resources by a set of ingress classes. The flag may be specified multiple times in order to allow multiple ingress classes. This source supports the --label-filter flag, which filters Ingress resources by a set of labels.","title":"Filtering the Ingresses considered"},{"location":"sources/ingress/#domain-names","text":"The domain names of the DNS entries created from an Ingress are sourced from the following places: Iterates over the Ingress\u2019s spec.rules , adding any non-empty host . This behavior is suppressed if the --ignore-ingress-rules-spec flag was specified or the Ingress had an external-dns.alpha.kubernetes.io/ingress-hostname-source: annotation-only annotation. Iterates over the Ingress\u2019s spec.tls , adding each member of hosts . This behavior is suppressed if the --ignore-ingress-tls-spec flag was specified or the Ingress had an external-dns.alpha.kubernetes.io/ingress-hostname-source: annotation-only annotation, Adds the hostnames from any external-dns.alpha.kubernetes.io/hostname annotation. This behavior is suppressed if the --ignore-hostname-annotation flag was specified or the Ingress had an external-dns.alpha.kubernetes.io/ingress-hostname-source: defined-hosts-only annotation. If no endpoints were produced for an Ingress by the previous steps or the --combine-fqdn-annotation flag was specified, then adds hostnames generated from any --fqdn-template flag.","title":"Domain names"},{"location":"sources/ingress/#targets","text":"The targets of the DNS entries created from an Ingress are sourced from the following places: If the Ingress has an external-dns.alpha.kubernetes.io/target annotation, uses the values from that. Otherwise, iterates over the Ingress\u2019s status.loadBalancer.ingress , adding each non-empty ip and hostname .","title":"Targets"},{"location":"sources/sources/","text":"Sources \u00b6 Source Resources annotation-filter label-filter ambassador-host Host.getambassador.io connector contour-httpproxy HttpProxy.projectcontour.io Yes cloudfoundry crd DNSEndpoint.externaldns.k8s.io Yes Yes f5-virtualserver VirtualServer.cis.f5.com Yes gateway-grpcroute GRPCRoute.gateway.networking.k8s.io Yes Yes gateway-httproute HTTPRoute.gateway.networking.k8s.io Yes Yes gateway-tcproute TCPRoute.gateway.networking.k8s.io Yes Yes gateway-tlsroute TLSRoute.gateway.networking.k8s.io Yes Yes gateway-udproute UDPRoute.gateway.networking.k8s.io Yes Yes gloo-proxy Proxy.gloo.solo.io ingress Ingress.networking.k8s.io Yes Yes istio-gateway Gateway.networking.istio.io Yes istio-virtualservice VirtualService.networking.istio.io Yes kong-tcpingress TCPIngress.configuration.konghq.com Yes node Node Yes openshift-route Route.route.openshift.io Yes Yes pod Pod service Service Yes Yes skipper-routegroup RouteGroup.zalando.org Yes traefik-proxy IngressRoute.traefik.io IngressRouteTCP.traefik.io IngressRouteUDP.traefik.io Yes","title":"About"},{"location":"sources/sources/#sources","text":"Source Resources annotation-filter label-filter ambassador-host Host.getambassador.io connector contour-httpproxy HttpProxy.projectcontour.io Yes cloudfoundry crd DNSEndpoint.externaldns.k8s.io Yes Yes f5-virtualserver VirtualServer.cis.f5.com Yes gateway-grpcroute GRPCRoute.gateway.networking.k8s.io Yes Yes gateway-httproute HTTPRoute.gateway.networking.k8s.io Yes Yes gateway-tcproute TCPRoute.gateway.networking.k8s.io Yes Yes gateway-tlsroute TLSRoute.gateway.networking.k8s.io Yes Yes gateway-udproute UDPRoute.gateway.networking.k8s.io Yes Yes gloo-proxy Proxy.gloo.solo.io ingress Ingress.networking.k8s.io Yes Yes istio-gateway Gateway.networking.istio.io Yes istio-virtualservice VirtualService.networking.istio.io Yes kong-tcpingress TCPIngress.configuration.konghq.com Yes node Node Yes openshift-route Route.route.openshift.io Yes Yes pod Pod service Service Yes Yes skipper-routegroup RouteGroup.zalando.org Yes traefik-proxy IngressRoute.traefik.io IngressRouteTCP.traefik.io IngressRouteUDP.traefik.io Yes","title":"Sources"},{"location":"tutorials/ANS_Group_SafeDNS/","text":"Setting up ExternalDNS for Services on ANS Group\u2019s SafeDNS \u00b6 This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using SafeDNS. Make sure to use >=0.11.0 version of ExternalDNS for this tutorial. Managing DNS with SafeDNS \u00b6 If you want to learn about how to use the SafeDNS service read the following tutorials: To learn more about the use of SafeDNS in general, see the following page: ANS Group\u2019s SafeDNS documentation . Creating SafeDNS credentials \u00b6 Generate a fresh API token for use with ExternalDNS, following the instructions at the ANS Group developer Getting-Started page. You will need to grant read/write access to the SafeDNS API. No access to any other ANS Group service is required. The environment variable SAFEDNS_TOKEN must have a value of this token to run ExternalDNS with SafeDNS integration. Deploy ExternalDNS \u00b6 Connect your kubectl client to the cluster you want to test ExternalDNS with. Then apply one of the following manifests file to deploy ExternalDNS. Manifest (for clusters without RBAC enabled) \u00b6 apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns # You will need to check what the latest version is yourself: # https://github.com/kubernetes-sigs/external-dns/releases image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible # (optional) limit to only example.com domains; change to match the # zone created above. - --domain-filter=example.com - --provider=safedns env : - name : SAFEDNS_TOKEN value : \"SAFEDNSTOKENSAFEDNSTOKEN\" Manifest (for clusters with RBAC enabled) \u00b6 apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible # (optional) limit to only example.com domains; change to match the # zone created above. - --domain-filter=example.com - --provider=safedns env : - name : SAFEDNS_TOKEN value : \"SAFEDNSTOKENSAFEDNSTOKEN\" Deploying an Nginx Service \u00b6 Create a service file called \u2018nginx.yaml\u2019 with the following contents: apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 --- apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : my-app.example.com spec : selector : app : nginx type : LoadBalancer ports : - protocol : TCP port : 80 targetPort : 80 Note the annotation on the service; use a hostname that matches the domain filter specified above. ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records. Create the deployment and service: $ kubectl create -f nginx.yaml Depending where you run your service it can take a little while for your cloud provider to create an external IP for the service. Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize the SafeDNS records. Verifying SafeDNS records \u00b6 Check your SafeDNS UI and select the appropriate domain to view the records for your SafeDNS zone. This should show the external IP address of the service as the A record for your domain. Alternatively, you can perform a DNS lookup for the hostname specified: $ dig +short my-app.example.com an.ip.addr.ess Cleanup \u00b6 Now that we have verified that ExternalDNS will automatically manage SafeDNS records, we can delete the tutorial\u2019s example: $ kubectl delete service -f nginx.yaml $ kubectl delete service -f externaldns.yaml","title":"Setting up ExternalDNS for Services on ANS Group's SafeDNS"},{"location":"tutorials/ANS_Group_SafeDNS/#setting-up-externaldns-for-services-on-ans-groups-safedns","text":"This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using SafeDNS. Make sure to use >=0.11.0 version of ExternalDNS for this tutorial.","title":"Setting up ExternalDNS for Services on ANS Group's SafeDNS"},{"location":"tutorials/ANS_Group_SafeDNS/#managing-dns-with-safedns","text":"If you want to learn about how to use the SafeDNS service read the following tutorials: To learn more about the use of SafeDNS in general, see the following page: ANS Group\u2019s SafeDNS documentation .","title":"Managing DNS with SafeDNS"},{"location":"tutorials/ANS_Group_SafeDNS/#creating-safedns-credentials","text":"Generate a fresh API token for use with ExternalDNS, following the instructions at the ANS Group developer Getting-Started page. You will need to grant read/write access to the SafeDNS API. No access to any other ANS Group service is required. The environment variable SAFEDNS_TOKEN must have a value of this token to run ExternalDNS with SafeDNS integration.","title":"Creating SafeDNS credentials"},{"location":"tutorials/ANS_Group_SafeDNS/#deploy-externaldns","text":"Connect your kubectl client to the cluster you want to test ExternalDNS with. Then apply one of the following manifests file to deploy ExternalDNS.","title":"Deploy ExternalDNS"},{"location":"tutorials/ANS_Group_SafeDNS/#manifest-for-clusters-without-rbac-enabled","text":"apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns # You will need to check what the latest version is yourself: # https://github.com/kubernetes-sigs/external-dns/releases image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible # (optional) limit to only example.com domains; change to match the # zone created above. - --domain-filter=example.com - --provider=safedns env : - name : SAFEDNS_TOKEN value : \"SAFEDNSTOKENSAFEDNSTOKEN\"","title":"Manifest (for clusters without RBAC enabled)"},{"location":"tutorials/ANS_Group_SafeDNS/#manifest-for-clusters-with-rbac-enabled","text":"apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible # (optional) limit to only example.com domains; change to match the # zone created above. - --domain-filter=example.com - --provider=safedns env : - name : SAFEDNS_TOKEN value : \"SAFEDNSTOKENSAFEDNSTOKEN\"","title":"Manifest (for clusters with RBAC enabled)"},{"location":"tutorials/ANS_Group_SafeDNS/#deploying-an-nginx-service","text":"Create a service file called \u2018nginx.yaml\u2019 with the following contents: apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 --- apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : my-app.example.com spec : selector : app : nginx type : LoadBalancer ports : - protocol : TCP port : 80 targetPort : 80 Note the annotation on the service; use a hostname that matches the domain filter specified above. ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records. Create the deployment and service: $ kubectl create -f nginx.yaml Depending where you run your service it can take a little while for your cloud provider to create an external IP for the service. Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize the SafeDNS records.","title":"Deploying an Nginx Service"},{"location":"tutorials/ANS_Group_SafeDNS/#verifying-safedns-records","text":"Check your SafeDNS UI and select the appropriate domain to view the records for your SafeDNS zone. This should show the external IP address of the service as the A record for your domain. Alternatively, you can perform a DNS lookup for the hostname specified: $ dig +short my-app.example.com an.ip.addr.ess","title":"Verifying SafeDNS records"},{"location":"tutorials/ANS_Group_SafeDNS/#cleanup","text":"Now that we have verified that ExternalDNS will automatically manage SafeDNS records, we can delete the tutorial\u2019s example: $ kubectl delete service -f nginx.yaml $ kubectl delete service -f externaldns.yaml","title":"Cleanup"},{"location":"tutorials/akamai-edgedns/","text":"Setting up External-DNS for Services on Akamai Edge DNS \u00b6 Prerequisites \u00b6 External-DNS v0.8.0 or greater. Zones \u00b6 External-DNS manages service endpoints in existing DNS zones. The Akamai provider does not add, remove or configure new zones. The Akamai Control Center or Akamai DevOps Tools , Akamai CLI and Akamai Terraform Provider can create and manage Edge DNS zones. Akamai Edge DNS Authentication \u00b6 The Akamai Edge DNS provider requires valid Akamai Edgegrid API authentication credentials to access zones and manage DNS records. Either directly by key or indirectly via a file can set credentials for the provider. The Akamai credential keys and mappings to the Akamai provider utilizing different presentation methods are: Edgegrid Auth Key External-DNS Cmd Line Key Environment/ConfigMap Key Description host akamai-serviceconsumerdomain EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN Akamai Edgegrid API server access_token akamai-access-token EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN Akamai Edgegrid API access token client_token akamai-client-token EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN Akamai Edgegrid API client token client-secret akamai-client-secret EXTERNAL_DNS_AKAMAI_CLIENT_SECRET Akamai Edgegrid API client secret In addition to specifying auth credentials individually, an Akamai Edgegrid .edgerc file convention can set credentials. External-DNS Cmd Line Environment/ConfigMap Description akamai-edgerc-path EXTERNAL_DNS_AKAMAI_EDGERC_PATH Accessible path to Edgegrid credentials file, e.g /home/test/.edgerc akamai-edgerc-section EXTERNAL_DNS_AKAMAI_EDGERC_SECTION Section in Edgegrid credentials file containing credentials Akamai API Authentication provides an overview and further information about authorization credentials for API base applications and tools. Deploy External-DNS \u00b6 An operational External-DNS deployment consists of an External-DNS container and service. The following sections demonstrate the ConfigMap objects that would make up an example functional external DNS kubernetes configuration utilizing NGINX as the service. Connect your kubectl client to the External-DNS cluster, and then apply one of the following manifest files: Manifest (for clusters without RBAC enabled) \u00b6 apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # or ingress or both - --provider=akamai - --domain-filter=example.com # zone-id-filter may be specified as well to filter on contract ID - --registry=txt - --txt-owner-id={{ owner-id-for-this-external-dns }} - --txt-prefix={{ prefix label for TXT record }}. env : - name : EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN valueFrom : secretKeyRef : name : external-dns key : EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN - name : EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN valueFrom : secretKeyRef : name : external-dns key : EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN - name : EXTERNAL_DNS_AKAMAI_CLIENT_SECRET valueFrom : secretKeyRef : name : external-dns key : EXTERNAL_DNS_AKAMAI_CLIENT_SECRET - name : EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN valueFrom : secretKeyRef : name : external-dns key : EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN Manifest (for clusters with RBAC enabled) \u00b6 apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"watch\" , \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # or ingress or both - --provider=akamai - --domain-filter=example.com # zone-id-filter may be specified as well to filter on contract ID - --registry=txt - --txt-owner-id={{ owner-id-for-this-external-dns }} - --txt-prefix={{ prefix label for TXT record }}. env : - name : EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN valueFrom : secretKeyRef : name : external-dns key : EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN - name : EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN valueFrom : secretKeyRef : name : external-dns key : EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN - name : EXTERNAL_DNS_AKAMAI_CLIENT_SECRET valueFrom : secretKeyRef : name : external-dns key : EXTERNAL_DNS_AKAMAI_CLIENT_SECRET - name : EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN valueFrom : secretKeyRef : name : external-dns key : EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN Create the deployment for External-DNS: $ kubectl apply -f externaldns.yaml Deploying an Nginx Service \u00b6 Create a service file called \u2018nginx.yaml\u2019 with the following contents: apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 --- apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : nginx.example.com external-dns.alpha.kubernetes.io/ttl : \"600\" #optional spec : selector : app : nginx type : LoadBalancer ports : - protocol : TCP port : 80 targetPort : 80 Create the deployment and service object: $ kubectl apply -f nginx.yaml Verify Akamai Edge DNS Records \u00b6 Wait 3-5 minutes before validating the records to allow the record changes to propagate to all the Akamai name servers. Validate records using the Akamai Control Center or by executing a dig, nslookup or similar DNS command. Cleanup \u00b6 Once you successfully configure and verify record management via External-DNS, you can delete the tutorial\u2019s examples: $ kubectl delete -f nginx.yaml $ kubectl delete -f externaldns.yaml Additional Information \u00b6 The Akamai provider allows the administrative user to filter zones by both name ( domain-filter ) and contract Id ( zone-id-filter ). The Edge DNS API will return a \u2018500 Internal Error\u2019 for invalid contract Ids. The provider will substitute quotes in TXT records with a ` (back tick) when writing records with the API.","title":"Setting up External-DNS for Services on Akamai Edge DNS"},{"location":"tutorials/akamai-edgedns/#setting-up-external-dns-for-services-on-akamai-edge-dns","text":"","title":"Setting up External-DNS for Services on Akamai Edge DNS"},{"location":"tutorials/akamai-edgedns/#prerequisites","text":"External-DNS v0.8.0 or greater.","title":"Prerequisites"},{"location":"tutorials/akamai-edgedns/#zones","text":"External-DNS manages service endpoints in existing DNS zones. The Akamai provider does not add, remove or configure new zones. The Akamai Control Center or Akamai DevOps Tools , Akamai CLI and Akamai Terraform Provider can create and manage Edge DNS zones.","title":"Zones"},{"location":"tutorials/akamai-edgedns/#akamai-edge-dns-authentication","text":"The Akamai Edge DNS provider requires valid Akamai Edgegrid API authentication credentials to access zones and manage DNS records. Either directly by key or indirectly via a file can set credentials for the provider. The Akamai credential keys and mappings to the Akamai provider utilizing different presentation methods are: Edgegrid Auth Key External-DNS Cmd Line Key Environment/ConfigMap Key Description host akamai-serviceconsumerdomain EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN Akamai Edgegrid API server access_token akamai-access-token EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN Akamai Edgegrid API access token client_token akamai-client-token EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN Akamai Edgegrid API client token client-secret akamai-client-secret EXTERNAL_DNS_AKAMAI_CLIENT_SECRET Akamai Edgegrid API client secret In addition to specifying auth credentials individually, an Akamai Edgegrid .edgerc file convention can set credentials. External-DNS Cmd Line Environment/ConfigMap Description akamai-edgerc-path EXTERNAL_DNS_AKAMAI_EDGERC_PATH Accessible path to Edgegrid credentials file, e.g /home/test/.edgerc akamai-edgerc-section EXTERNAL_DNS_AKAMAI_EDGERC_SECTION Section in Edgegrid credentials file containing credentials Akamai API Authentication provides an overview and further information about authorization credentials for API base applications and tools.","title":"Akamai Edge DNS Authentication"},{"location":"tutorials/akamai-edgedns/#deploy-external-dns","text":"An operational External-DNS deployment consists of an External-DNS container and service. The following sections demonstrate the ConfigMap objects that would make up an example functional external DNS kubernetes configuration utilizing NGINX as the service. Connect your kubectl client to the External-DNS cluster, and then apply one of the following manifest files:","title":"Deploy External-DNS"},{"location":"tutorials/akamai-edgedns/#manifest-for-clusters-without-rbac-enabled","text":"apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # or ingress or both - --provider=akamai - --domain-filter=example.com # zone-id-filter may be specified as well to filter on contract ID - --registry=txt - --txt-owner-id={{ owner-id-for-this-external-dns }} - --txt-prefix={{ prefix label for TXT record }}. env : - name : EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN valueFrom : secretKeyRef : name : external-dns key : EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN - name : EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN valueFrom : secretKeyRef : name : external-dns key : EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN - name : EXTERNAL_DNS_AKAMAI_CLIENT_SECRET valueFrom : secretKeyRef : name : external-dns key : EXTERNAL_DNS_AKAMAI_CLIENT_SECRET - name : EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN valueFrom : secretKeyRef : name : external-dns key : EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN","title":"Manifest (for clusters without RBAC enabled)"},{"location":"tutorials/akamai-edgedns/#manifest-for-clusters-with-rbac-enabled","text":"apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"watch\" , \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # or ingress or both - --provider=akamai - --domain-filter=example.com # zone-id-filter may be specified as well to filter on contract ID - --registry=txt - --txt-owner-id={{ owner-id-for-this-external-dns }} - --txt-prefix={{ prefix label for TXT record }}. env : - name : EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN valueFrom : secretKeyRef : name : external-dns key : EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN - name : EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN valueFrom : secretKeyRef : name : external-dns key : EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN - name : EXTERNAL_DNS_AKAMAI_CLIENT_SECRET valueFrom : secretKeyRef : name : external-dns key : EXTERNAL_DNS_AKAMAI_CLIENT_SECRET - name : EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN valueFrom : secretKeyRef : name : external-dns key : EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN Create the deployment for External-DNS: $ kubectl apply -f externaldns.yaml","title":"Manifest (for clusters with RBAC enabled)"},{"location":"tutorials/akamai-edgedns/#deploying-an-nginx-service","text":"Create a service file called \u2018nginx.yaml\u2019 with the following contents: apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 --- apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : nginx.example.com external-dns.alpha.kubernetes.io/ttl : \"600\" #optional spec : selector : app : nginx type : LoadBalancer ports : - protocol : TCP port : 80 targetPort : 80 Create the deployment and service object: $ kubectl apply -f nginx.yaml","title":"Deploying an Nginx Service"},{"location":"tutorials/akamai-edgedns/#verify-akamai-edge-dns-records","text":"Wait 3-5 minutes before validating the records to allow the record changes to propagate to all the Akamai name servers. Validate records using the Akamai Control Center or by executing a dig, nslookup or similar DNS command.","title":"Verify Akamai Edge DNS Records"},{"location":"tutorials/akamai-edgedns/#cleanup","text":"Once you successfully configure and verify record management via External-DNS, you can delete the tutorial\u2019s examples: $ kubectl delete -f nginx.yaml $ kubectl delete -f externaldns.yaml","title":"Cleanup"},{"location":"tutorials/akamai-edgedns/#additional-information","text":"The Akamai provider allows the administrative user to filter zones by both name ( domain-filter ) and contract Id ( zone-id-filter ). The Edge DNS API will return a \u2018500 Internal Error\u2019 for invalid contract Ids. The provider will substitute quotes in TXT records with a ` (back tick) when writing records with the API.","title":"Additional Information"},{"location":"tutorials/alibabacloud/","text":"Setting up ExternalDNS for Services on Alibaba Cloud \u00b6 This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster on Alibaba Cloud. Make sure to use >=0.5.6 version of ExternalDNS for this tutorial RAM Permissions \u00b6 { \"Version\" : \"1\" , \"Statement\" : [ { \"Action\" : \"alidns:AddDomainRecord\" , \"Resource\" : \"*\" , \"Effect\" : \"Allow\" }, { \"Action\" : \"alidns:DeleteDomainRecord\" , \"Resource\" : \"*\" , \"Effect\" : \"Allow\" }, { \"Action\" : \"alidns:UpdateDomainRecord\" , \"Resource\" : \"*\" , \"Effect\" : \"Allow\" }, { \"Action\" : \"alidns:DescribeDomainRecords\" , \"Resource\" : \"*\" , \"Effect\" : \"Allow\" }, { \"Action\" : \"alidns:DescribeDomains\" , \"Resource\" : \"*\" , \"Effect\" : \"Allow\" }, { \"Action\" : \"pvtz:AddZoneRecord\" , \"Resource\" : \"*\" , \"Effect\" : \"Allow\" }, { \"Action\" : \"pvtz:DeleteZoneRecord\" , \"Resource\" : \"*\" , \"Effect\" : \"Allow\" }, { \"Action\" : \"pvtz:UpdateZoneRecord\" , \"Resource\" : \"*\" , \"Effect\" : \"Allow\" }, { \"Action\" : \"pvtz:DescribeZoneRecords\" , \"Resource\" : \"*\" , \"Effect\" : \"Allow\" }, { \"Action\" : \"pvtz:DescribeZones\" , \"Resource\" : \"*\" , \"Effect\" : \"Allow\" }, { \"Action\" : \"pvtz:DescribeZoneInfo\" , \"Resource\" : \"*\" , \"Effect\" : \"Allow\" } ] } When running on Alibaba Cloud, you need to make sure that your nodes (on which External DNS runs) have the RAM instance profile with the above RAM role assigned. Set up a Alibaba Cloud DNS service or Private Zone service \u00b6 Alibaba Cloud DNS Service is the domain name resolution and management service for public access. It routes access from end-users to the designated web app. Alibaba Cloud Private Zone is the domain name resolution and management service for VPC internal access. If you prefer to try-out ExternalDNS in one of the existing domain or zone you can skip this step Create a DNS domain which will contain the managed DNS records. For public DNS service, the domain name should be valid and owned by yourself. $ aliyun alidns AddDomain --DomainName \"external-dns-test.com\" Make a note of the ID of the hosted zone you just created. $ aliyun alidns DescribeDomains --KeyWord = \"external-dns-test.com\" | jq -r '.Domains.Domain[0].DomainId' Deploy ExternalDNS \u00b6 Connect your kubectl client to the cluster you want to test ExternalDNS with. Then apply one of the following manifests file to deploy ExternalDNS. Manifest (for clusters without RBAC enabled) \u00b6 apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service - --source=ingress - --domain-filter=external-dns-test.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones - --provider=alibabacloud - --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization - --alibaba-cloud-zone-type=public # only look at public hosted zones (valid values are public, private or no value for both) - --registry=txt - --txt-owner-id=my-identifier volumeMounts : - mountPath : /usr/share/zoneinfo name : hostpath volumes : - name : hostpath hostPath : path : /usr/share/zoneinfo type : Directory Manifest (for clusters with RBAC enabled) \u00b6 apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service - --source=ingress - --domain-filter=external-dns-test.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones - --provider=alibabacloud - --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization - --alibaba-cloud-zone-type=public # only look at public hosted zones (valid values are public, private or no value for both) - --registry=txt - --txt-owner-id=my-identifier - --alibaba-cloud-config-file= # enable sts token volumeMounts : - mountPath : /usr/share/zoneinfo name : hostpath volumes : - name : hostpath hostPath : path : /usr/share/zoneinfo type : Directory Arguments \u00b6 This list is not the full list, but a few arguments that where chosen. alibaba-cloud-zone-type \u00b6 alibaba-cloud-zone-type allows filtering for private and public zones If value is public , it will sync with records in Alibaba Cloud DNS Service If value is private , it will sync with records in Alibaba Cloud Private Zone Service Verify ExternalDNS works (Ingress example) \u00b6 Create an ingress resource manifest file. For ingress objects ExternalDNS will create a DNS record based on the host specified for the ingress object. apiVersion : networking.k8s.io/v1 kind : Ingress metadata : name : foo spec : ingressClassName : nginx # use the one that corresponds to your ingress controller. rules : - host : foo.external-dns-test.com http : paths : - backend : service : name : foo port : number : 80 pathType : Prefix Verify ExternalDNS works (Service example) \u00b6 Create the following sample application to test that ExternalDNS works. For services ExternalDNS will look for the annotation external-dns.alpha.kubernetes.io/hostname on the service and use the corresponding value. apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : nginx.external-dns-test.com. spec : type : LoadBalancer ports : - port : 80 name : http targetPort : 80 selector : app : nginx --- apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 name : http After roughly two minutes check that a corresponding DNS record for your service was created. $ aliyun alidns DescribeDomainRecords --DomainName = external-dns-test.com { \"PageNumber\": 1, \"TotalCount\": 1, \"PageSize\": 20, \"RequestId\": \"1DBEF426-F771-46C7-9802-4989E9C94EE8\", \"DomainRecords\": { \"Record\": [ { \"RR\": \"nginx\", \"Status\": \"ENABLE\", \"Value\": \"1.2.3.4\", \"Weight\": 1, \"RecordId\": \"3994015629411328\", \"Type\": \"A\", \"DomainName\": \"external-dns-test.com\", \"Locked\": false, \"Line\": \"default\", \"TTL\": 600 }\uff0c { \"RR\": \"nginx\", \"Status\": \"ENABLE\", \"Value\": \"heritage=external-dns;external-dns/owner=my-identifier\", \"Weight\": 1, \"RecordId\": \"3994015629411329\", \"Type\": \"TTL\", \"DomainName\": \"external-dns-test.com\", \"Locked\": false, \"Line\": \"default\", \"TTL\": 600 } ] } } Note created TXT record alongside ALIAS record. TXT record signifies that the corresponding ALIAS record is managed by ExternalDNS. This makes ExternalDNS safe for running in environments where there are other records managed via other means. Let\u2019s check that we can resolve this DNS name. We\u2019ll ask the nameservers assigned to your zone first. $ dig nginx.external-dns-test.com. If you hooked up your DNS zone with its parent zone correctly you can use curl to access your site. $ curl nginx.external-dns-test.com. Welcome to nginx! ... ... Custom TTL \u00b6 The default DNS record TTL (Time-To-Live) is 300 seconds. You can customize this value by setting the annotation external-dns.alpha.kubernetes.io/ttl . e.g., modify the service manifest YAML file above: apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : nginx.external-dns-test.com external-dns.alpha.kubernetes.io/ttl : 60 spec : ... This will set the DNS record\u2019s TTL to 60 seconds. Clean up \u00b6 Make sure to delete all Service objects before terminating the cluster so all load balancers get cleaned up correctly. $ kubectl delete service nginx Give ExternalDNS some time to clean up the DNS records for you. Then delete the hosted zone if you created one for the testing purpose. $ aliyun alidns DeleteDomain --DomainName external-dns-test.com For more info about Alibaba Cloud external dns, please refer this docs","title":"Setting up ExternalDNS for Services on Alibaba Cloud"},{"location":"tutorials/alibabacloud/#setting-up-externaldns-for-services-on-alibaba-cloud","text":"This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster on Alibaba Cloud. Make sure to use >=0.5.6 version of ExternalDNS for this tutorial","title":"Setting up ExternalDNS for Services on Alibaba Cloud"},{"location":"tutorials/alibabacloud/#ram-permissions","text":"{ \"Version\" : \"1\" , \"Statement\" : [ { \"Action\" : \"alidns:AddDomainRecord\" , \"Resource\" : \"*\" , \"Effect\" : \"Allow\" }, { \"Action\" : \"alidns:DeleteDomainRecord\" , \"Resource\" : \"*\" , \"Effect\" : \"Allow\" }, { \"Action\" : \"alidns:UpdateDomainRecord\" , \"Resource\" : \"*\" , \"Effect\" : \"Allow\" }, { \"Action\" : \"alidns:DescribeDomainRecords\" , \"Resource\" : \"*\" , \"Effect\" : \"Allow\" }, { \"Action\" : \"alidns:DescribeDomains\" , \"Resource\" : \"*\" , \"Effect\" : \"Allow\" }, { \"Action\" : \"pvtz:AddZoneRecord\" , \"Resource\" : \"*\" , \"Effect\" : \"Allow\" }, { \"Action\" : \"pvtz:DeleteZoneRecord\" , \"Resource\" : \"*\" , \"Effect\" : \"Allow\" }, { \"Action\" : \"pvtz:UpdateZoneRecord\" , \"Resource\" : \"*\" , \"Effect\" : \"Allow\" }, { \"Action\" : \"pvtz:DescribeZoneRecords\" , \"Resource\" : \"*\" , \"Effect\" : \"Allow\" }, { \"Action\" : \"pvtz:DescribeZones\" , \"Resource\" : \"*\" , \"Effect\" : \"Allow\" }, { \"Action\" : \"pvtz:DescribeZoneInfo\" , \"Resource\" : \"*\" , \"Effect\" : \"Allow\" } ] } When running on Alibaba Cloud, you need to make sure that your nodes (on which External DNS runs) have the RAM instance profile with the above RAM role assigned.","title":"RAM Permissions"},{"location":"tutorials/alibabacloud/#set-up-a-alibaba-cloud-dns-service-or-private-zone-service","text":"Alibaba Cloud DNS Service is the domain name resolution and management service for public access. It routes access from end-users to the designated web app. Alibaba Cloud Private Zone is the domain name resolution and management service for VPC internal access. If you prefer to try-out ExternalDNS in one of the existing domain or zone you can skip this step Create a DNS domain which will contain the managed DNS records. For public DNS service, the domain name should be valid and owned by yourself. $ aliyun alidns AddDomain --DomainName \"external-dns-test.com\" Make a note of the ID of the hosted zone you just created. $ aliyun alidns DescribeDomains --KeyWord = \"external-dns-test.com\" | jq -r '.Domains.Domain[0].DomainId'","title":"Set up a Alibaba Cloud DNS service or Private Zone service"},{"location":"tutorials/alibabacloud/#deploy-externaldns","text":"Connect your kubectl client to the cluster you want to test ExternalDNS with. Then apply one of the following manifests file to deploy ExternalDNS.","title":"Deploy ExternalDNS"},{"location":"tutorials/alibabacloud/#manifest-for-clusters-without-rbac-enabled","text":"apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service - --source=ingress - --domain-filter=external-dns-test.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones - --provider=alibabacloud - --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization - --alibaba-cloud-zone-type=public # only look at public hosted zones (valid values are public, private or no value for both) - --registry=txt - --txt-owner-id=my-identifier volumeMounts : - mountPath : /usr/share/zoneinfo name : hostpath volumes : - name : hostpath hostPath : path : /usr/share/zoneinfo type : Directory","title":"Manifest (for clusters without RBAC enabled)"},{"location":"tutorials/alibabacloud/#manifest-for-clusters-with-rbac-enabled","text":"apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service - --source=ingress - --domain-filter=external-dns-test.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones - --provider=alibabacloud - --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization - --alibaba-cloud-zone-type=public # only look at public hosted zones (valid values are public, private or no value for both) - --registry=txt - --txt-owner-id=my-identifier - --alibaba-cloud-config-file= # enable sts token volumeMounts : - mountPath : /usr/share/zoneinfo name : hostpath volumes : - name : hostpath hostPath : path : /usr/share/zoneinfo type : Directory","title":"Manifest (for clusters with RBAC enabled)"},{"location":"tutorials/alibabacloud/#arguments","text":"This list is not the full list, but a few arguments that where chosen.","title":"Arguments"},{"location":"tutorials/alibabacloud/#alibaba-cloud-zone-type","text":"alibaba-cloud-zone-type allows filtering for private and public zones If value is public , it will sync with records in Alibaba Cloud DNS Service If value is private , it will sync with records in Alibaba Cloud Private Zone Service","title":"alibaba-cloud-zone-type"},{"location":"tutorials/alibabacloud/#verify-externaldns-works-ingress-example","text":"Create an ingress resource manifest file. For ingress objects ExternalDNS will create a DNS record based on the host specified for the ingress object. apiVersion : networking.k8s.io/v1 kind : Ingress metadata : name : foo spec : ingressClassName : nginx # use the one that corresponds to your ingress controller. rules : - host : foo.external-dns-test.com http : paths : - backend : service : name : foo port : number : 80 pathType : Prefix","title":"Verify ExternalDNS works (Ingress example)"},{"location":"tutorials/alibabacloud/#verify-externaldns-works-service-example","text":"Create the following sample application to test that ExternalDNS works. For services ExternalDNS will look for the annotation external-dns.alpha.kubernetes.io/hostname on the service and use the corresponding value. apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : nginx.external-dns-test.com. spec : type : LoadBalancer ports : - port : 80 name : http targetPort : 80 selector : app : nginx --- apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 name : http After roughly two minutes check that a corresponding DNS record for your service was created. $ aliyun alidns DescribeDomainRecords --DomainName = external-dns-test.com { \"PageNumber\": 1, \"TotalCount\": 1, \"PageSize\": 20, \"RequestId\": \"1DBEF426-F771-46C7-9802-4989E9C94EE8\", \"DomainRecords\": { \"Record\": [ { \"RR\": \"nginx\", \"Status\": \"ENABLE\", \"Value\": \"1.2.3.4\", \"Weight\": 1, \"RecordId\": \"3994015629411328\", \"Type\": \"A\", \"DomainName\": \"external-dns-test.com\", \"Locked\": false, \"Line\": \"default\", \"TTL\": 600 }\uff0c { \"RR\": \"nginx\", \"Status\": \"ENABLE\", \"Value\": \"heritage=external-dns;external-dns/owner=my-identifier\", \"Weight\": 1, \"RecordId\": \"3994015629411329\", \"Type\": \"TTL\", \"DomainName\": \"external-dns-test.com\", \"Locked\": false, \"Line\": \"default\", \"TTL\": 600 } ] } } Note created TXT record alongside ALIAS record. TXT record signifies that the corresponding ALIAS record is managed by ExternalDNS. This makes ExternalDNS safe for running in environments where there are other records managed via other means. Let\u2019s check that we can resolve this DNS name. We\u2019ll ask the nameservers assigned to your zone first. $ dig nginx.external-dns-test.com. If you hooked up your DNS zone with its parent zone correctly you can use curl to access your site. $ curl nginx.external-dns-test.com. Welcome to nginx! ... ... ","title":"Verify ExternalDNS works (Service example)"},{"location":"tutorials/alibabacloud/#custom-ttl","text":"The default DNS record TTL (Time-To-Live) is 300 seconds. You can customize this value by setting the annotation external-dns.alpha.kubernetes.io/ttl . e.g., modify the service manifest YAML file above: apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : nginx.external-dns-test.com external-dns.alpha.kubernetes.io/ttl : 60 spec : ... This will set the DNS record\u2019s TTL to 60 seconds.","title":"Custom TTL"},{"location":"tutorials/alibabacloud/#clean-up","text":"Make sure to delete all Service objects before terminating the cluster so all load balancers get cleaned up correctly. $ kubectl delete service nginx Give ExternalDNS some time to clean up the DNS records for you. Then delete the hosted zone if you created one for the testing purpose. $ aliyun alidns DeleteDomain --DomainName external-dns-test.com For more info about Alibaba Cloud external dns, please refer this docs","title":"Clean up"},{"location":"tutorials/aws-load-balancer-controller/","text":"Using ExternalDNS with aws-load-balancer-controller \u00b6 This tutorial describes how to use ExternalDNS with the aws-load-balancer-controller . Setting up ExternalDNS and aws-load-balancer-controller \u00b6 Follow the AWS tutorial to setup ExternalDNS for use in Kubernetes clusters running in AWS. Specify the source=ingress argument so that ExternalDNS will look for hostnames in Ingress objects. In addition, you may wish to limit which Ingress objects are used as an ExternalDNS source via the ingress-class argument, but this is not required. For help setting up the AWS Load Balancer Controller, follow the Setup Guide . Note that the AWS Load Balancer Controller uses the same tags for subnet auto-discovery as Kubernetes does with the AWS cloud provider. In the examples that follow, it is assumed that you configured the ALB Ingress Controller with the ingress-class=alb argument (not to be confused with the same argument to ExternalDNS) so that the controller will only respect Ingress objects with the ingressClassName field set to \u201calb\u201d. Deploy an example application \u00b6 Create the following sample \u201cechoserver\u201d application to demonstrate how ExternalDNS works with ALB ingress objects. apiVersion : apps/v1 kind : Deployment metadata : name : echoserver spec : replicas : 1 selector : matchLabels : app : echoserver template : metadata : labels : app : echoserver spec : containers : - image : gcr.io/google_containers/echoserver:1.4 imagePullPolicy : Always name : echoserver ports : - containerPort : 8080 --- apiVersion : v1 kind : Service metadata : name : echoserver spec : ports : - port : 80 targetPort : 8080 protocol : TCP type : NodePort selector : app : echoserver Note that the Service object is of type NodePort . We don\u2019t need a Service of type LoadBalancer here, since we will be using an Ingress to create an ALB. Ingress examples \u00b6 Create the following Ingress to expose the echoserver application to the Internet. apiVersion : networking.k8s.io/v1 kind : Ingress metadata : annotations : alb.ingress.kubernetes.io/scheme : internet-facing name : echoserver spec : ingressClassName : alb rules : - host : echoserver.mycluster.example.org http : &echoserver_root paths : - path : / backend : service : name : echoserver port : number : 80 pathType : Prefix - host : echoserver.example.org http : *echoserver_root The above should result in the creation of an (ipv4) ALB in AWS which will forward traffic to the echoserver application. If the source=ingress argument is specified, then ExternalDNS will create DNS records based on the hosts specified in ingress objects. The above example would result in two alias records being created, echoserver.mycluster.example.org and echoserver.example.org , which both alias the ALB that is associated with the Ingress object. Note that the above example makes use of the YAML anchor feature to avoid having to repeat the http section for multiple hosts that use the exact same paths. If this Ingress object will only be fronting one backend Service, we might instead create the following: apiVersion : networking.k8s.io/v1 kind : Ingress metadata : annotations : alb.ingress.kubernetes.io/scheme : internet-facing external-dns.alpha.kubernetes.io/hostname : echoserver.mycluster.example.org, echoserver.example.org name : echoserver spec : ingressClassName : alb rules : - http : paths : - path : / backend : service : name : echoserver port : number : 80 pathType : Prefix In the above example we create a default path that works for any hostname, and make use of the external-dns.alpha.kubernetes.io/hostname annotation to create multiple aliases for the resulting ALB. Dualstack ALBs \u00b6 AWS supports both IPv4 and \u201cdualstack\u201d (both IPv4 and IPv6) interfaces for ALBs. The AWS Load Balancer Controller uses the alb.ingress.kubernetes.io/ip-address-type annotation (which defaults to ipv4 ) to determine this. If this annotation is set to dualstack then ExternalDNS will create two alias records (one A record and one AAAA record) for each hostname associated with the Ingress object. Example: apiVersion : networking.k8s.io/v1 kind : Ingress metadata : annotations : alb.ingress.kubernetes.io/scheme : internet-facing alb.ingress.kubernetes.io/ip-address-type : dualstack name : echoserver spec : ingressClassName : alb rules : - host : echoserver.example.org http : paths : - path : / backend : service : name : echoserver port : number : 80 pathType : Prefix The above Ingress object will result in the creation of an ALB with a dualstack interface. ExternalDNS will create both an A echoserver.example.org record and an AAAA record of the same name, that each are aliases for the same ALB.","title":"Using ExternalDNS with aws-load-balancer-controller"},{"location":"tutorials/aws-load-balancer-controller/#using-externaldns-with-aws-load-balancer-controller","text":"This tutorial describes how to use ExternalDNS with the aws-load-balancer-controller .","title":"Using ExternalDNS with aws-load-balancer-controller"},{"location":"tutorials/aws-load-balancer-controller/#setting-up-externaldns-and-aws-load-balancer-controller","text":"Follow the AWS tutorial to setup ExternalDNS for use in Kubernetes clusters running in AWS. Specify the source=ingress argument so that ExternalDNS will look for hostnames in Ingress objects. In addition, you may wish to limit which Ingress objects are used as an ExternalDNS source via the ingress-class argument, but this is not required. For help setting up the AWS Load Balancer Controller, follow the Setup Guide . Note that the AWS Load Balancer Controller uses the same tags for subnet auto-discovery as Kubernetes does with the AWS cloud provider. In the examples that follow, it is assumed that you configured the ALB Ingress Controller with the ingress-class=alb argument (not to be confused with the same argument to ExternalDNS) so that the controller will only respect Ingress objects with the ingressClassName field set to \u201calb\u201d.","title":"Setting up ExternalDNS and aws-load-balancer-controller"},{"location":"tutorials/aws-load-balancer-controller/#deploy-an-example-application","text":"Create the following sample \u201cechoserver\u201d application to demonstrate how ExternalDNS works with ALB ingress objects. apiVersion : apps/v1 kind : Deployment metadata : name : echoserver spec : replicas : 1 selector : matchLabels : app : echoserver template : metadata : labels : app : echoserver spec : containers : - image : gcr.io/google_containers/echoserver:1.4 imagePullPolicy : Always name : echoserver ports : - containerPort : 8080 --- apiVersion : v1 kind : Service metadata : name : echoserver spec : ports : - port : 80 targetPort : 8080 protocol : TCP type : NodePort selector : app : echoserver Note that the Service object is of type NodePort . We don\u2019t need a Service of type LoadBalancer here, since we will be using an Ingress to create an ALB.","title":"Deploy an example application"},{"location":"tutorials/aws-load-balancer-controller/#ingress-examples","text":"Create the following Ingress to expose the echoserver application to the Internet. apiVersion : networking.k8s.io/v1 kind : Ingress metadata : annotations : alb.ingress.kubernetes.io/scheme : internet-facing name : echoserver spec : ingressClassName : alb rules : - host : echoserver.mycluster.example.org http : &echoserver_root paths : - path : / backend : service : name : echoserver port : number : 80 pathType : Prefix - host : echoserver.example.org http : *echoserver_root The above should result in the creation of an (ipv4) ALB in AWS which will forward traffic to the echoserver application. If the source=ingress argument is specified, then ExternalDNS will create DNS records based on the hosts specified in ingress objects. The above example would result in two alias records being created, echoserver.mycluster.example.org and echoserver.example.org , which both alias the ALB that is associated with the Ingress object. Note that the above example makes use of the YAML anchor feature to avoid having to repeat the http section for multiple hosts that use the exact same paths. If this Ingress object will only be fronting one backend Service, we might instead create the following: apiVersion : networking.k8s.io/v1 kind : Ingress metadata : annotations : alb.ingress.kubernetes.io/scheme : internet-facing external-dns.alpha.kubernetes.io/hostname : echoserver.mycluster.example.org, echoserver.example.org name : echoserver spec : ingressClassName : alb rules : - http : paths : - path : / backend : service : name : echoserver port : number : 80 pathType : Prefix In the above example we create a default path that works for any hostname, and make use of the external-dns.alpha.kubernetes.io/hostname annotation to create multiple aliases for the resulting ALB.","title":"Ingress examples"},{"location":"tutorials/aws-load-balancer-controller/#dualstack-albs","text":"AWS supports both IPv4 and \u201cdualstack\u201d (both IPv4 and IPv6) interfaces for ALBs. The AWS Load Balancer Controller uses the alb.ingress.kubernetes.io/ip-address-type annotation (which defaults to ipv4 ) to determine this. If this annotation is set to dualstack then ExternalDNS will create two alias records (one A record and one AAAA record) for each hostname associated with the Ingress object. Example: apiVersion : networking.k8s.io/v1 kind : Ingress metadata : annotations : alb.ingress.kubernetes.io/scheme : internet-facing alb.ingress.kubernetes.io/ip-address-type : dualstack name : echoserver spec : ingressClassName : alb rules : - host : echoserver.example.org http : paths : - path : / backend : service : name : echoserver port : number : 80 pathType : Prefix The above Ingress object will result in the creation of an ALB with a dualstack interface. ExternalDNS will create both an A echoserver.example.org record and an AAAA record of the same name, that each are aliases for the same ALB.","title":"Dualstack ALBs"},{"location":"tutorials/aws-sd/","text":"Setting up ExternalDNS using AWS Cloud Map API \u00b6 This tutorial describes how to set up ExternalDNS for usage within a Kubernetes cluster with AWS Cloud Map API . AWS Cloud Map API is an alternative approach to managing DNS records directly using the Route53 API. It is more suitable for a dynamic environment where service endpoints change frequently. It abstracts away technical details of the DNS protocol and offers a simplified model. AWS Cloud Map consists of three main API calls: CreatePublicDnsNamespace \u2013 automatically creates a DNS hosted zone CreateService \u2013 creates a new named service inside the specified namespace RegisterInstance/DeregisterInstance \u2013 can be called multiple times to create a DNS record for the specified Service Learn more about the API in the AWS Cloud Map API Reference . IAM Permissions \u00b6 To use the AWS Cloud Map API, a user must have permissions to create the DNS namespace. Additionally you need to make sure that your nodes (on which External DNS runs) have an IAM instance profile with the AWSCloudMapFullAccess managed policy attached, that provides following permissions: { \"Version\": \"2012-10-17\", \"Statement\": [ { \"Effect\": \"Allow\", \"Action\": [ \"route53:GetHostedZone\", \"route53:ListHostedZonesByName\", \"route53:CreateHostedZone\", \"route53:DeleteHostedZone\", \"route53:ChangeResourceRecordSets\", \"route53:CreateHealthCheck\", \"route53:GetHealthCheck\", \"route53:DeleteHealthCheck\", \"route53:UpdateHealthCheck\", \"ec2:DescribeVpcs\", \"ec2:DescribeRegions\", \"servicediscovery:*\" ], \"Resource\": [ \"*\" ] } ] } Set up a namespace \u00b6 Create a DNS namespace using the AWS Cloud Map API: $ aws servicediscovery create-public-dns-namespace --name \"external-dns-test.my-org.com\" Verify that the namespace was truly created $ aws servicediscovery list-namespaces Deploy ExternalDNS \u00b6 Connect your kubectl client to the cluster that you want to test ExternalDNS with. Then apply the following manifest file to deploy ExternalDNS. Manifest (for clusters without RBAC enabled) \u00b6 apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 env : - name : AWS_REGION value : us-east-1 # put your CloudMap NameSpace region args : - --source=service - --source=ingress - --domain-filter=external-dns-test.my-org.com # Makes ExternalDNS see only the namespaces that match the specified domain. Omit the filter if you want to process all available namespaces. - --provider=aws-sd - --aws-zone-type=public # Only look at public namespaces. Valid values are public, private, or no value for both) - --txt-owner-id=my-identifier Manifest (for clusters with RBAC enabled) \u00b6 apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" , \"watch\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 env : - name : AWS_REGION value : us-east-1 # put your CloudMap NameSpace region args : - --source=service - --source=ingress - --domain-filter=external-dns-test.my-org.com # Makes ExternalDNS see only the namespaces that match the specified domain. Omit the filter if you want to process all available namespaces. - --provider=aws-sd - --aws-zone-type=public # Only look at public namespaces. Valid values are public, private, or no value for both) - --txt-owner-id=my-identifier Verify that ExternalDNS works (Service example) \u00b6 Create the following sample application to test that ExternalDNS works. For services ExternalDNS will look for the annotation external-dns.alpha.kubernetes.io/hostname on the service and use the corresponding value. apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : nginx.external-dns-test.my-org.com spec : type : LoadBalancer ports : - port : 80 name : http targetPort : 80 selector : app : nginx --- apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 name : http After one minute check that a corresponding DNS record for your service was created in your hosted zone. We recommended that you use the Amazon Route53 console for that purpose. Custom TTL \u00b6 The default DNS record TTL (time to live) is 300 seconds. You can customize this value by setting the annotation external-dns.alpha.kubernetes.io/ttl . For example, modify the service manifest YAML file above: apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : nginx.external-dns-test.my-org.com external-dns.alpha.kubernetes.io/ttl : 60 spec : ... This will set the TTL for the DNS record to 60 seconds. Clean up \u00b6 Delete all service objects before terminating the cluster so all load balancers get cleaned up correctly. $ kubectl delete service nginx Give ExternalDNS some time to clean up the DNS records for you. Then delete the remaining service and namespace. $ aws servicediscovery list-services { \"Services\": [ { \"Id\": \"srv-6dygt5ywvyzvi3an\", \"Arn\": \"arn:aws:servicediscovery:us-west-2:861574988794:service/srv-6dygt5ywvyzvi3an\", \"Name\": \"nginx\" } ] } $ aws servicediscovery delete-service --id srv-6dygt5ywvyzvi3an $ aws servicediscovery list-namespaces { \"Namespaces\": [ { \"Type\": \"DNS_PUBLIC\", \"Id\": \"ns-durf2oxu4gxcgo6z\", \"Arn\": \"arn:aws:servicediscovery:us-west-2:861574988794:namespace/ns-durf2oxu4gxcgo6z\", \"Name\": \"external-dns-test.my-org.com\" } ] } $ aws servicediscovery delete-namespace --id ns-durf2oxu4gxcgo6z","title":"Setting up ExternalDNS using AWS Cloud Map API"},{"location":"tutorials/aws-sd/#setting-up-externaldns-using-aws-cloud-map-api","text":"This tutorial describes how to set up ExternalDNS for usage within a Kubernetes cluster with AWS Cloud Map API . AWS Cloud Map API is an alternative approach to managing DNS records directly using the Route53 API. It is more suitable for a dynamic environment where service endpoints change frequently. It abstracts away technical details of the DNS protocol and offers a simplified model. AWS Cloud Map consists of three main API calls: CreatePublicDnsNamespace \u2013 automatically creates a DNS hosted zone CreateService \u2013 creates a new named service inside the specified namespace RegisterInstance/DeregisterInstance \u2013 can be called multiple times to create a DNS record for the specified Service Learn more about the API in the AWS Cloud Map API Reference .","title":"Setting up ExternalDNS using AWS Cloud Map API"},{"location":"tutorials/aws-sd/#iam-permissions","text":"To use the AWS Cloud Map API, a user must have permissions to create the DNS namespace. Additionally you need to make sure that your nodes (on which External DNS runs) have an IAM instance profile with the AWSCloudMapFullAccess managed policy attached, that provides following permissions: { \"Version\": \"2012-10-17\", \"Statement\": [ { \"Effect\": \"Allow\", \"Action\": [ \"route53:GetHostedZone\", \"route53:ListHostedZonesByName\", \"route53:CreateHostedZone\", \"route53:DeleteHostedZone\", \"route53:ChangeResourceRecordSets\", \"route53:CreateHealthCheck\", \"route53:GetHealthCheck\", \"route53:DeleteHealthCheck\", \"route53:UpdateHealthCheck\", \"ec2:DescribeVpcs\", \"ec2:DescribeRegions\", \"servicediscovery:*\" ], \"Resource\": [ \"*\" ] } ] }","title":"IAM Permissions"},{"location":"tutorials/aws-sd/#set-up-a-namespace","text":"Create a DNS namespace using the AWS Cloud Map API: $ aws servicediscovery create-public-dns-namespace --name \"external-dns-test.my-org.com\" Verify that the namespace was truly created $ aws servicediscovery list-namespaces","title":"Set up a namespace"},{"location":"tutorials/aws-sd/#deploy-externaldns","text":"Connect your kubectl client to the cluster that you want to test ExternalDNS with. Then apply the following manifest file to deploy ExternalDNS.","title":"Deploy ExternalDNS"},{"location":"tutorials/aws-sd/#manifest-for-clusters-without-rbac-enabled","text":"apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 env : - name : AWS_REGION value : us-east-1 # put your CloudMap NameSpace region args : - --source=service - --source=ingress - --domain-filter=external-dns-test.my-org.com # Makes ExternalDNS see only the namespaces that match the specified domain. Omit the filter if you want to process all available namespaces. - --provider=aws-sd - --aws-zone-type=public # Only look at public namespaces. Valid values are public, private, or no value for both) - --txt-owner-id=my-identifier","title":"Manifest (for clusters without RBAC enabled)"},{"location":"tutorials/aws-sd/#manifest-for-clusters-with-rbac-enabled","text":"apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" , \"watch\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 env : - name : AWS_REGION value : us-east-1 # put your CloudMap NameSpace region args : - --source=service - --source=ingress - --domain-filter=external-dns-test.my-org.com # Makes ExternalDNS see only the namespaces that match the specified domain. Omit the filter if you want to process all available namespaces. - --provider=aws-sd - --aws-zone-type=public # Only look at public namespaces. Valid values are public, private, or no value for both) - --txt-owner-id=my-identifier","title":"Manifest (for clusters with RBAC enabled)"},{"location":"tutorials/aws-sd/#verify-that-externaldns-works-service-example","text":"Create the following sample application to test that ExternalDNS works. For services ExternalDNS will look for the annotation external-dns.alpha.kubernetes.io/hostname on the service and use the corresponding value. apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : nginx.external-dns-test.my-org.com spec : type : LoadBalancer ports : - port : 80 name : http targetPort : 80 selector : app : nginx --- apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 name : http After one minute check that a corresponding DNS record for your service was created in your hosted zone. We recommended that you use the Amazon Route53 console for that purpose.","title":"Verify that ExternalDNS works (Service example)"},{"location":"tutorials/aws-sd/#custom-ttl","text":"The default DNS record TTL (time to live) is 300 seconds. You can customize this value by setting the annotation external-dns.alpha.kubernetes.io/ttl . For example, modify the service manifest YAML file above: apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : nginx.external-dns-test.my-org.com external-dns.alpha.kubernetes.io/ttl : 60 spec : ... This will set the TTL for the DNS record to 60 seconds.","title":"Custom TTL"},{"location":"tutorials/aws-sd/#clean-up","text":"Delete all service objects before terminating the cluster so all load balancers get cleaned up correctly. $ kubectl delete service nginx Give ExternalDNS some time to clean up the DNS records for you. Then delete the remaining service and namespace. $ aws servicediscovery list-services { \"Services\": [ { \"Id\": \"srv-6dygt5ywvyzvi3an\", \"Arn\": \"arn:aws:servicediscovery:us-west-2:861574988794:service/srv-6dygt5ywvyzvi3an\", \"Name\": \"nginx\" } ] } $ aws servicediscovery delete-service --id srv-6dygt5ywvyzvi3an $ aws servicediscovery list-namespaces { \"Namespaces\": [ { \"Type\": \"DNS_PUBLIC\", \"Id\": \"ns-durf2oxu4gxcgo6z\", \"Arn\": \"arn:aws:servicediscovery:us-west-2:861574988794:namespace/ns-durf2oxu4gxcgo6z\", \"Name\": \"external-dns-test.my-org.com\" } ] } $ aws servicediscovery delete-namespace --id ns-durf2oxu4gxcgo6z","title":"Clean up"},{"location":"tutorials/aws/","text":"Setting up ExternalDNS for Services on AWS \u00b6 This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster on AWS. Make sure to use >=0.11.0 version of ExternalDNS for this tutorial IAM Policy \u00b6 The following IAM Policy document allows ExternalDNS to update Route53 Resource Record Sets and Hosted Zones. You\u2019ll want to create this Policy in IAM first. In our example, we\u2019ll call the policy AllowExternalDNSUpdates (but you can call it whatever you prefer). If you prefer, you may fine-tune the policy to permit updates only to explicit Hosted Zone IDs. { \"Version\" : \"2012-10-17\" , \"Statement\" : [ { \"Effect\" : \"Allow\" , \"Action\" : [ \"route53:ChangeResourceRecordSets\" ], \"Resource\" : [ \"arn:aws:route53:::hostedzone/*\" ] }, { \"Effect\" : \"Allow\" , \"Action\" : [ \"route53:ListHostedZones\" , \"route53:ListResourceRecordSets\" , \"route53:ListTagsForResource\" ], \"Resource\" : [ \"*\" ] } ] } If you are using the AWS CLI, you can run the following to install the above policy (saved as policy.json ). This can be use in subsequent steps to allow ExternalDNS to access Route53 zones. aws iam create-policy --policy-name \"AllowExternalDNSUpdates\" --policy-document file://policy.json # example: arn:aws:iam::XXXXXXXXXXXX:policy/AllowExternalDNSUpdates export POLICY_ARN = $( aws iam list-policies \\ --query 'Policies[?PolicyName==`AllowExternalDNSUpdates`].Arn' --output text ) Provisioning a Kubernetes cluster \u00b6 You can use eksctl to easily provision an Amazon Elastic Kubernetes Service ( EKS ) cluster that is suitable for this tutorial. See Getting started with Amazon EKS \u2013 eksctl . export EKS_CLUSTER_NAME = \"my-externaldns-cluster\" export EKS_CLUSTER_REGION = \"us-east-2\" export KUBECONFIG = \" $HOME /.kube/ ${ EKS_CLUSTER_NAME } - ${ EKS_CLUSTER_REGION } .yaml\" eksctl create cluster --name $EKS_CLUSTER_NAME --region $EKS_CLUSTER_REGION Feel free to use other provisioning tools or an existing cluster. If Terraform is used, vpc and eks modules are recommended for standing up an EKS cluster. Amazon has a workshop called Amazon EKS Terraform Workshop that may be useful for this process. Permissions to modify DNS zone \u00b6 You will need to use the above policy (represented by the POLICY_ARN environment variable) to allow ExternalDNS to update records in Route53 DNS zones. Here are three common ways this can be accomplished: Node IAM Role Static credentials IAM Roles for Service Accounts For this tutorial, ExternalDNS will use the environment variable EXTERNALDNS_NS to represent the namespace, defaulted to default . Feel free to change this to something else, such externaldns or kube-addons . Make sure to edit the subjects[0].namespace for the ClusterRoleBinding resource when deploying ExternalDNS with RBAC enabled. See Manifest (for clusters with RBAC enabled) for more information. Additionally, throughout this tutorial, the example domain of example.com is used. Change this to appropriate domain under your control. See Set up a hosted zone section. Node IAM Role \u00b6 In this method, you can attach a policy to the Node IAM Role. This will allow nodes in the Kubernetes cluster to access Route53 zones, which allows ExternalDNS to update DNS records. Given that this allows all containers to access Route53, not just ExternalDNS, running on the node with these privileges, this method is not recommended, and is only suitable for limited limited test environments. If you are using eksctl to provision a new cluster, you add the policy at creation time with: eksctl create cluster --external-dns-access \\ --name $EKS_CLUSTER_NAME --region $EKS_CLUSTER_REGION \\ WARNING : This will assign allow read-write access to all nodes in the cluster, not just ExternalDNS. For this reason, this method is only suitable for limited test environments. If you already provisioned a cluster or use other provisioning tools like Terraform, you can use AWS CLI to attach the policy to the Node IAM Role. Get the Node IAM role name \u00b6 The role name of the role associated with the node(s) where ExternalDNS will run is needed. An easy way to get the role name is to use the AWS web console (https://console.aws.amazon.com/eks/), and find any instance in the target node group and copy the role name associated with that instance. Get role name with a single managed nodegroup \u00b6 From the command line, if you have a single managed node group, the default with eksctl create cluster , you can find the role name with the following: # get managed node group name (assuming there's only one node group) GROUP_NAME = $( aws eks list-nodegroups --cluster-name $EKS_CLUSTER_NAME \\ --query nodegroups --out text ) # fetch role arn given node group name ROLE_ARN = $( aws eks describe-nodegroup --cluster-name $EKS_CLUSTER_NAME \\ --nodegroup-name $GROUP_NAME --query nodegroup.nodeRole --out text ) # extract just the name part of role arn ROLE_NAME = ${ NODE_ROLE_ARN ##*/ } Get role name with other configurations \u00b6 If you have multiple node groups or any unmanaged node groups, the process gets more complex. The first step is to get the instance host name of the desired node to where ExternalDNS will be deployed or is already deployed: # node instance name of one of the external dns pods currently running INSTANCE_NAME = $( kubectl get pods --all-namespaces \\ --selector app.kubernetes.io/instance = external-dns \\ --output jsonpath = '{.items[0].spec.nodeName}' ) # instance name of one of the nodes (change if node group is different) INSTANCE_NAME = $( kubectl get nodes --output name | cut -d '/' -f2 | tail -1 ) With the instance host name, you can then get the instance id: get_instance_id () { INSTANCE_NAME = $1 # example: ip-192-168-74-34.us-east-2.compute.internal # get list of nodes # ip-192-168-74-34.us-east-2.compute.internal aws:///us-east-2a/i-xxxxxxxxxxxxxxxxx # ip-192-168-86-105.us-east-2.compute.internal aws:///us-east-2a/i-xxxxxxxxxxxxxxxxx NODES = $( kubectl get nodes \\ --output jsonpath = '{range .items[*]}{.metadata.name}{\"\\t\"}{.spec.providerID}{\"\\n\"}{end}' ) # print instance id from matching node grep $INSTANCE_NAME <<< \" $NODES \" | cut -d '/' -f5 } INSTANCE_ID = $( get_instance_id $INSTANCE_NAME ) With the instance id, you can get the associated role name: findRoleName () { INSTANCE_ID = $1 # get all of the roles ROLES =( $( aws iam list-roles --query Roles [ * ] .RoleName --out text ) ) for ROLE in ${ ROLES [*] } ; do # get instance profile arn PROFILE_ARN = $( aws iam list-instance-profiles-for-role \\ --role-name $ROLE --query InstanceProfiles [ 0 ] .Arn --output text ) # if there is an instance profile if [[ \" $PROFILE_ARN \" ! = \"None\" ]] ; then # get all the instances with this associated instance profile INSTANCES = $( aws ec2 describe-instances \\ --filters Name = iam-instance-profile.arn,Values = $PROFILE_ARN \\ --query Reservations [ * ] .Instances [ 0 ] .InstanceId --out text ) # find instances that match the instant profile for INSTANCE in ${ INSTANCES [*] } ; do # set role name value if there is a match if [[ \" $INSTANCE_ID \" == \" $INSTANCE \" ]] ; then ROLE_NAME = $ROLE ; fi done fi done echo $ROLE_NAME } NODE_ROLE_NAME = $( findRoleName $INSTANCE_ID ) Using the role name, you can associate the policy that was created earlier: # attach policy arn created earlier to node IAM role aws iam attach-role-policy --role-name $NODE_ROLE_NAME --policy-arn $POLICY_ARN WARNING : This will assign allow read-write access to all pods running on the same node pool, not just the ExternalDNS pod(s). Deploy ExternalDNS with attached policy to Node IAM Role \u00b6 If ExternalDNS is not yet deployed, follow the steps under Deploy ExternalDNS using either RBAC or non-RBAC. NOTE : Before deleting the cluster during, be sure to run aws iam detach-role-policy . Otherwise, there can be errors as the provisioning system, such as eksctl or terraform , will not be able to delete the roles with the attached policy. Static credentials \u00b6 In this method, the policy is attached to an IAM user, and the credentials secrets for the IAM user are then made available using a Kubernetes secret. This method is not the preferred method as the secrets in the credential file could be copied and used by an unauthorized threat actor. However, if the Kubernetes cluster is not hosted on AWS, it may be the only method available. Given this situation, it is important to limit the associated privileges to just minimal required privileges, i.e. read-write access to Route53, and not used a credentials file that has extra privileges beyond what is required. Create IAM user and attach the policy \u00b6 # create IAM user aws iam create-user --user-name \"externaldns\" # attach policy arn created earlier to IAM user aws iam attach-user-policy --user-name \"externaldns\" --policy-arn $POLICY_ARN Create the static credentials \u00b6 SECRET_ACCESS_KEY = $( aws iam create-access-key --user-name \"externaldns\" ) cat <<-EOF > /local/path/to/credentials [default] aws_access_key_id = $(echo $SECRET_ACCESS_KEY | jq -r '.AccessKey.AccessKeyId') aws_secret_access_key = $(echo $SECRET_ACCESS_KEY | jq -r '.AccessKey.SecretAccessKey') EOF Create Kubernetes secret from credentials \u00b6 kubectl create secret generic external-dns \\ --namespace ${ EXTERNALDNS_NS :- \"default\" } --from-file /local/path/to/credentials Deploy ExternalDNS using static credentials \u00b6 Follow the steps under Deploy ExternalDNS using either RBAC or non-RBAC. Make sure to uncomment the section that mounts volumes, so that the credentials can be mounted. IAM Roles for Service Accounts \u00b6 IRSA ( IAM roles for Service Accounts ) allows cluster operators to map AWS IAM Roles to Kubernetes Service Accounts. This essentially allows only ExternalDNS pods to access Route53 without exposing any static credentials. This is the preferred method as it implements PoLP ( Principal of Least Privilege ). IMPORTANT : This method requires using KSA (Kuberntes service account) and RBAC. This method requires deploying with RBAC. See Manifest (for clusters with RBAC enabled) when ready to deploy ExternalDNS. NOTE : Similar methods to IRSA on AWS are kiam , which is in maintenence mode, and has instructions for creating an IAM role, and also kube2iam . IRSA is the officially supported method for EKS clusters, and so for non-EKS clusters on AWS, these other tools could be an option. Verify OIDC is supported \u00b6 aws eks describe-cluster --name $EKS_CLUSTER_NAME \\ --query \"cluster.identity.oidc.issuer\" --output text Associate OIDC to cluster \u00b6 Configure the cluster with an OIDC provider and add support for IRSA ( IAM roles for Service Accounts ). If you used eksctl to provision the EKS cluster, you can update it with the following command: eksctl utils associate-iam-oidc-provider \\ --cluster $EKS_CLUSTER_NAME --approve If the cluster was provisioned with Terraform, you can use the iam_openid_connect_provider resource ( ref ) to associate to the OIDC provider. Create an IAM role bound to a service account \u00b6 For the next steps in this process, we will need to associate the external-dns service account and a role used to grant access to Route53. This requires the following steps: Create a role with a trust relationship to the cluster\u2019s OIDC provider Attach the AllowExternalDNSUpdates policy to the role Create the external-dns service account Add annotation to the service account with the role arn Use eksctl with eksctl created EKS cluster \u00b6 If eksctl was used to provision the EKS cluster, you can perform all of these steps with the following command: eksctl create iamserviceaccount \\ --cluster $EKS_CLUSTER_NAME \\ --name \"external-dns\" \\ --namespace ${ EXTERNALDNS_NS :- \"default\" } \\ --attach-policy-arn $POLICY_ARN \\ --approve Use aws cli with any EKS cluster \u00b6 Otherwise, we can do the following steps using aws commands (also see Creating an IAM role and policy for your service account ): ACCOUNT_ID = $( aws sts get-caller-identity \\ --query \"Account\" --output text ) OIDC_PROVIDER = $( aws eks describe-cluster --name $EKS_CLUSTER_NAME \\ --query \"cluster.identity.oidc.issuer\" --output text | sed -e 's|^https://||' ) cat <<-EOF > trust.json { \"Version\": \"2012-10-17\", \"Statement\": [ { \"Effect\": \"Allow\", \"Principal\": { \"Federated\": \"arn:aws:iam::$ACCOUNT_ID:oidc-provider/$OIDC_PROVIDER\" }, \"Action\": \"sts:AssumeRoleWithWebIdentity\", \"Condition\": { \"StringEquals\": { \"$OIDC_PROVIDER:sub\": \"system:serviceaccount:${EXTERNALDNS_NS:-\"default\"}:external-dns\", \"$OIDC_PROVIDER:aud\": \"sts.amazonaws.com\" } } } ] } EOF IRSA_ROLE = \"external-dns-irsa-role\" aws iam create-role --role-name $IRSA_ROLE --assume-role-policy-document file://trust.json aws iam attach-role-policy --role-name $IRSA_ROLE --policy-arn $POLICY_ARN ROLE_ARN = $( aws iam get-role --role-name $IRSA_ROLE --query Role.Arn --output text ) # Create service account (skip is already created) kubectl create serviceaccount \"external-dns\" --namespace ${ EXTERNALDNS_NS :- \"default\" } # Add annotation referencing IRSA role kubectl patch serviceaccount \"external-dns\" --namespace ${ EXTERNALDNS_NS :- \"default\" } --patch \\ \"{\\\"metadata\\\": { \\\"annotations\\\": { \\\"eks.amazonaws.com/role-arn\\\": \\\" $ROLE_ARN \\\" }}}\" If any part of this step is misconfigured, such as the role with incorrect namespace configured in the trust relationship, annotation pointing the the wrong role, etc., you will see errors like WebIdentityErr: failed to retrieve credentials . Check the configuration and make corrections. When the service account annotations are updated, then the current running pods will have to be terminated, so that new pod(s) with proper configuration (environment variables) will be created automatically. When annotation is added to service account, the ExternalDNS pod(s) scheduled will have AWS_ROLE_ARN , AWS_STS_REGIONAL_ENDPOINTS , and AWS_WEB_IDENTITY_TOKEN_FILE environment variables injected automatically. Deploy ExternalDNS using IRSA \u00b6 Follow the steps under Manifest (for clusters with RBAC enabled) . Make sure to comment out the service account section if this has been created already. If you deployed ExternalDNS before adding the service account annotation and the corresponding role, you will likely see error with failed to list hosted zones: AccessDenied: User . You can delete the current running ExternalDNS pod(s) after updating the annotation, so that new pods scheduled will have appropriate configuration to access Route53. Set up a hosted zone \u00b6 If you prefer to try-out ExternalDNS in one of the existing hosted-zones you can skip this step Create a DNS zone which will contain the managed DNS records. This tutorial will use the fictional domain of example.com . aws route53 create-hosted-zone --name \"example.com.\" \\ --caller-reference \"external-dns-test- $( date +%s ) \" Make a note of the nameservers that were assigned to your new zone. ZONE_ID = $( aws route53 list-hosted-zones-by-name --output json \\ --dns-name \"example.com.\" --query HostedZones [ 0 ] .Id --out text ) aws route53 list-resource-record-sets --output text \\ --hosted-zone-id $ZONE_ID --query \\ \"ResourceRecordSets[?Type == 'NS'].ResourceRecords[*].Value | []\" | tr '\\t' '\\n' This should yield something similar this: ns-695.awsdns-22.net. ns-1313.awsdns-36.org. ns-350.awsdns-43.com. ns-1805.awsdns-33.co.uk. If using your own domain that was registered with a third-party domain registrar, you should point your domain\u2019s name servers to the values in the from the list above. Please consult your registrar\u2019s documentation on how to do that. Deploy ExternalDNS \u00b6 Connect your kubectl client to the cluster you want to test ExternalDNS with. Then apply one of the following manifests file to deploy ExternalDNS. You can check if your cluster has RBAC by kubectl api-versions | grep rbac.authorization.k8s.io . For clusters with RBAC enabled, be sure to choose the correct namespace . For this tutorial, the enviornment variable EXTERNALDNS_NS will refer to the namespace. You can set this to a value of your choice: export EXTERNALDNS_NS = \"default\" # externaldns, kube-addons, etc # create namespace if it does not yet exist kubectl get namespaces | grep -q $EXTERNALDNS_NS || \\ kubectl create namespace $EXTERNALDNS_NS Manifest (for clusters without RBAC enabled) \u00b6 Save the following below as externaldns-no-rbac.yaml . apiVersion : apps/v1 kind : Deployment metadata : name : external-dns labels : app.kubernetes.io/name : external-dns spec : strategy : type : Recreate selector : matchLabels : app.kubernetes.io/name : external-dns template : metadata : labels : app.kubernetes.io/name : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service - --source=ingress - --domain-filter=example.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones - --provider=aws - --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization - --aws-zone-type=public # only look at public hosted zones (valid values are public, private or no value for both) - --registry=txt - --txt-owner-id=my-hostedzone-identifier env : - name : AWS_DEFAULT_REGION value : us-east-1 # change to region where EKS is installed # # Uncomment below if using static credentials # - name: AWS_SHARED_CREDENTIALS_FILE # value: /.aws/credentials # volumeMounts: # - name: aws-credentials # mountPath: /.aws # readOnly: true # volumes: # - name: aws-credentials # secret: # secretName: external-dns When ready you can deploy: kubectl create --filename externaldns-no-rbac.yaml \\ --namespace ${ EXTERNALDNS_NS :- \"default\" } Manifest (for clusters with RBAC enabled) \u00b6 Save the following below as externaldns-with-rbac.yaml . # comment out sa if it was previously created apiVersion : v1 kind : ServiceAccount metadata : name : external-dns labels : app.kubernetes.io/name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns labels : app.kubernetes.io/name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" , \"nodes\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer labels : app.kubernetes.io/name : external-dns roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default # change to desired namespace: externaldns, kube-addons --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns labels : app.kubernetes.io/name : external-dns spec : strategy : type : Recreate selector : matchLabels : app.kubernetes.io/name : external-dns template : metadata : labels : app.kubernetes.io/name : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service - --source=ingress - --domain-filter=example.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones - --provider=aws - --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization - --aws-zone-type=public # only look at public hosted zones (valid values are public, private or no value for both) - --registry=txt - --txt-owner-id=external-dns env : - name : AWS_DEFAULT_REGION value : us-east-1 # change to region where EKS is installed # # Uncommend below if using static credentials # - name: AWS_SHARED_CREDENTIALS_FILE # value: /.aws/credentials # volumeMounts: # - name: aws-credentials # mountPath: /.aws # readOnly: true # volumes: # - name: aws-credentials # secret: # secretName: external-dns When ready deploy: kubectl create --filename externaldns-with-rbac.yaml \\ --namespace ${ EXTERNALDNS_NS :- \"default\" } Arguments \u00b6 This list is not the full list, but a few arguments that where chosen. aws-zone-type \u00b6 aws-zone-type allows filtering for private and public zones Annotations \u00b6 Annotations which are specific to AWS. alias \u00b6 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. To make the target an alias, the ingress needs to be configured correctly as described in the docs . In particular, the argument --publish-service=default/nginx-ingress-controller has to be set on the nginx-ingress-controller container. If one uses the nginx-ingress Helm chart, this flag can be set with the controller.publishService.enabled configuration option. target-hosted-zone \u00b6 external-dns.alpha.kubernetes.io/aws-target-hosted-zone can optionally be set to the ID of a Route53 hosted zone. This will force external-dns to use the specified hosted zone when creating an ALIAS target. Verify ExternalDNS works (Service example) \u00b6 Create the following sample application to test that ExternalDNS works. For services ExternalDNS will look for the annotation external-dns.alpha.kubernetes.io/hostname on the service and use the corresponding value. If you want to give multiple names to service, you can set it to external-dns.alpha.kubernetes.io/hostname with a comma , separator. For this verification phase, you can use default or another namespace for the nginx demo, for example: NGINXDEMO_NS = \"nginx\" kubectl get namespaces | grep -q $NGINXDEMO_NS || kubectl create namespace $NGINXDEMO_NS Save the following manifest below as nginx.yaml : apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : nginx.example.com spec : type : LoadBalancer ports : - port : 80 name : http targetPort : 80 selector : app : nginx --- apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 name : http Deploy the nginx deployment and service with: kubectl create --filename nginx.yaml --namespace ${ NGINXDEMO_NS :- \"default\" } Verify that the load balancer was allocated with: kubectl get service nginx --namespace ${ NGINXDEMO_NS :- \"default\" } This should show something like: NAME TYPE CLUSTER-IP EXTERNAL-IP PORT ( S ) AGE nginx LoadBalancer 10 .100.47.41 ae11c2360188411e7951602725593fd1-1224345803.eu-central-1.elb.amazonaws.com. 80 :32749/TCP 12m After roughly two minutes check that a corresponding DNS record for your service that was created. aws route53 list-resource-record-sets --output json --hosted-zone-id $ZONE_ID \\ --query \"ResourceRecordSets[?Name == 'nginx.example.com.']|[?Type == 'A']\" This should show something like: [ { \"Name\" : \"nginx.example.com.\" , \"Type\" : \"A\" , \"AliasTarget\" : { \"HostedZoneId\" : \"ZEWFWZ4R16P7IB\" , \"DNSName\" : \"ae11c2360188411e7951602725593fd1-1224345803.eu-central-1.elb.amazonaws.com.\" , \"EvaluateTargetHealth\" : true } } ] You can also fetch the corresponding text records: aws route53 list-resource-record-sets --output json --hosted-zone-id $ZONE_ID \\ --query \"ResourceRecordSets[?Name == 'nginx.example.com.']|[?Type == 'TXT']\" This will show something like: [ { \"Name\" : \"nginx.example.com.\" , \"Type\" : \"TXT\" , \"TTL\" : 300 , \"ResourceRecords\" : [ { \"Value\" : \"\\\"heritage=external-dns,external-dns/owner=external-dns,external-dns/resource=service/default/nginx\\\"\" } ] } ] Note created TXT record alongside ALIAS record. TXT record signifies that the corresponding ALIAS record is managed by ExternalDNS. This makes ExternalDNS safe for running in environments where there are other records managed via other means. For more information about ALIAS record, see Choosing between alias and non-alias records . Let\u2019s check that we can resolve this DNS name. We\u2019ll ask the nameservers assigned to your zone first. dig +short @ns-5514.awsdns-53.org. nginx.example.com. This should return 1+ IP addresses that correspond to the ELB FQDN, i.e. ae11c2360188411e7951602725593fd1-1224345803.eu-central-1.elb.amazonaws.com. . Next try the public nameservers configured by DNS client on your system: dig +short nginx.example.com. If you hooked up your DNS zone with its parent zone correctly you can use curl to access your site. curl nginx.example.com. This should show something like: < html > < head > < title > Welcome to nginx! ... < body > < h1 > Welcome to nginx! ... Verify ExternalDNS works (Ingress example) \u00b6 With the previous deployment and service objects deployed, we can add an ingress object and configure a FQDN value for the host key. The ingress controller will match incoming HTTP traffic, and route it to the appropriate backend service based on the host key. For ingress objects ExternalDNS will create a DNS record based on the host specified for the ingress object. For this tutorial, we have two endpoints, the service with LoadBalancer type and an ingress. For practical purposes, if an ingress is used, the service type can be changed to ClusterIP as two endpoints are unecessary in this scenario. IMPORTANT : This requires that an ingress controller has been installed in your Kubernetes cluster. EKS does not come with an ingress controller by default. A popular ingress controller is ingress-nginx , which can be installed by a helm chart or by manifests . Create an ingress resource manifest file named ingress.yaml with the contents below: --- apiVersion : networking.k8s.io/v1 kind : Ingress metadata : name : nginx spec : ingressClassName : nginx rules : - host : server.example.com http : paths : - backend : service : name : nginx port : number : 80 path : / pathType : Prefix When ready, you can deploy this with: kubectl create --filename ingress.yaml --namespace ${ NGINXDEMO_NS :- \"default\" } Watch the status of the ingress until the ADDRESS field is populated. kubectl get ingress --watch --namespace ${ NGINXDEMO_NS :- \"default\" } You should see something like this: NAME CLASS HOSTS ADDRESS PORTS AGE nginx server.example.com 80 47s nginx server.example.com ae11c2360188411e7951602725593fd1-1224345803.eu-central-1.elb.amazonaws.com. 80 54s For the ingress test, run through similar checks, but using domain name used for the ingress: # check records on route53 aws route53 list-resource-record-sets --output json --hosted-zone-id $ZONE_ID \\ --query \"ResourceRecordSets[?Name == 'server.example.com.']\" # query using a route53 name server dig +short @ns-5514.awsdns-53.org. server.example.com. # query using the default name server dig +short server.example.com. # connect to the nginx web server through the ingress curl server.example.com. More service annotation options \u00b6 Custom TTL \u00b6 The default DNS record TTL (Time-To-Live) is 300 seconds. You can customize this value by setting the annotation external-dns.alpha.kubernetes.io/ttl . e.g., modify the service manifest YAML file above: apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : nginx.example.com external-dns.alpha.kubernetes.io/ttl : \"60\" spec : ... This will set the DNS record\u2019s TTL to 60 seconds. Routing policies \u00b6 Route53 offers different routing policies . The routing policy for a record can be controlled with the following annotations: external-dns.alpha.kubernetes.io/set-identifier : this needs to be set to use any of the following routing policies For any given DNS name, only one of the following routing policies can be used: Weighted records: external-dns.alpha.kubernetes.io/aws-weight Latency-based routing: external-dns.alpha.kubernetes.io/aws-region Failover: external-dns.alpha.kubernetes.io/aws-failover Geolocation-based routing: external-dns.alpha.kubernetes.io/aws-geolocation-continent-code external-dns.alpha.kubernetes.io/aws-geolocation-country-code external-dns.alpha.kubernetes.io/aws-geolocation-subdivision-code Multi-value answer: external-dns.alpha.kubernetes.io/aws-multi-value-answer Associating DNS records with healthchecks \u00b6 You can configure Route53 to associate DNS records with healthchecks for automated DNS failover using external-dns.alpha.kubernetes.io/aws-health-check-id: annotation. Note: ExternalDNS does not support creating healthchecks, and assumes that already exists. Canonical Hosted Zones \u00b6 When creating ALIAS type records in Route53 it is required that external-dns be aware of the canonical hosted zone in which the specified hostname is created. External-dns is able to automatically identify the canonical hosted zone for many hostnames based upon known hostname suffixes which are defined in aws.go . If a hostname does not have a known suffix then the suffix can be added into aws.go or the target-hosted-zone annotation can be used to manually define the ID of the canonical hosted zone. Govcloud caveats \u00b6 Due to the special nature with how Route53 runs in Govcloud, there are a few tweaks in the deployment settings. An Environment variable with name of AWS_REGION set to either us-gov-west-1 or us-gov-east-1 is required. Otherwise it tries to lookup a region that does not exist in Govcloud and it errors out. env : - name : AWS_REGION value : us-gov-west-1 Route53 in Govcloud does not allow aliases. Therefore, container args must be set so that it uses CNAMES and a txt-prefix must be set to something. Otherwise, it will try to create a TXT record with the same value than the CNAME itself, which is not allowed. args : - --aws-prefer-cname - --txt-prefix={{ YOUR_PREFIX }} The first two changes are needed if you use Route53 in Govcloud, which only supports private zones. There are also no cross account IAM whatsoever between Govcloud and commercial AWS accounts. If services and ingresses need to make Route 53 entries to an public zone in a commercial account, you will have set env variables of AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY with a key and secret to the commercial account that has the sufficient rights. env : - name : AWS_ACCESS_KEY_ID value : XXXXXXXXX - name : AWS_SECRET_ACCESS_KEY valueFrom : secretKeyRef : name : {{ YOUR_SECRET_NAME }} key : {{ YOUR_SECRET_KEY }} Clean up \u00b6 Make sure to delete all Service objects before terminating the cluster so all load balancers get cleaned up correctly. kubectl delete service nginx IMPORTANT If you attached a policy to the Node IAM Role, then you will want to detach this before deleting the EKS cluster. Otherwise, the role resource will be locked, and the cluster cannot be deleted, especially if it was provisioned by automation like terraform or eksctl . aws iam detach-role-policy --role-name $NODE_ROLE_NAME --policy-arn $POLICY_ARN If the cluster was provisioned using eksctl , you can delete the cluster with: eksctl delete cluster --name $EKS_CLUSTER_NAME --region $EKS_CLUSTER_REGION Give ExternalDNS some time to clean up the DNS records for you. Then delete the hosted zone if you created one for the testing purpose. aws route53 delete-hosted-zone --id $NODE_ID # e.g /hostedzone/ZEWFWZ4R16P7IB If IAM user credentials were used, you can remove the user with: aws iam detach-user-policy --user-name \"externaldns\" --policy-arn $POLICY_ARN aws iam delete-user --user-name \"externaldns\" If IRSA was used, you can remove the IRSA role with: aws iam detach-role-policy --role-name $IRSA_ROLE --policy-arn $POLICY_ARN aws iam delete-role --role-name $IRSA_ROLE Delete any unneeded policies: aws iam delete-policy --policy-arn $POLICY_ARN Throttling \u00b6 Route53 has a 5 API requests per second per account hard quota . Running several fast polling ExternalDNS instances in a given account can easily hit that limit. Some ways to reduce the request rate include: * Reduce the polling loop\u2019s synchronization interval at the possible cost of slower change propagation (but see --events below to reduce the impact). * --interval=5m (default 1m ) * Trigger the polling loop on changes to K8s objects, rather than only at interval , to have responsive updates with long poll intervals * --events * Limit the sources watched when the --events flag is specified to specific types, namespaces, labels, or annotations * --source=ingress --source=service - specify multiple times for multiple sources * --namespace=my-app * --label-filter=app in (my-app) * --ingress-class=nginx-external * Limit services watched by type (not applicable to ingress or other types) * --service-type-filter=LoadBalancer default all * Limit the hosted zones considered * --zone-id-filter=ABCDEF12345678 - specify multiple times if needed * --domain-filter=example.com by domain suffix - specify multiple times if needed * --regex-domain-filter=example* by domain suffix but as a regex - overrides domain-filter * --exclude-domains=ignore.this.example.com to exclude a domain or subdomain * --regex-domain-exclusion=ignore* subtracts it\u2019s matches from regex-domain-filter \u2019s matches * --aws-zone-type=public only sync zones of this type [public|private] * --aws-zone-tags=owner=k8s only sync zones with this tag * If the list of zones managed by ExternalDNS doesn\u2019t change frequently, cache it by setting a TTL. * --aws-zones-cache-duration=3h (default 0 - disabled) * Increase the number of changes applied to Route53 in each batch * --aws-batch-change-size=4000 (default 1000 ) * Increase the interval between changes * --aws-batch-change-interval=10s (default 1s ) * Introducing some jitter to the pod initialization, so that when multiple instances of ExternalDNS are updated at the same time they do not make their requests on the same second. A simple way to implement randomised startup is with an init container: ... spec: initContainers: - name: init-jitter image: registry.k8s.io/external-dns/external-dns:v0.13.5 command: - /bin/sh - -c - 'FOR=$((RANDOM % 10))s;echo \"Sleeping for $FOR\";sleep $FOR' containers: ... EKS \u00b6 An effective starting point for EKS with an ingress controller might look like: --interval = 5m --events --source = ingress --domain-filter = example.com --aws-zones-cache-duration = 1h","title":"Setting up ExternalDNS for Services on AWS"},{"location":"tutorials/aws/#setting-up-externaldns-for-services-on-aws","text":"This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster on AWS. Make sure to use >=0.11.0 version of ExternalDNS for this tutorial","title":"Setting up ExternalDNS for Services on AWS"},{"location":"tutorials/aws/#iam-policy","text":"The following IAM Policy document allows ExternalDNS to update Route53 Resource Record Sets and Hosted Zones. You\u2019ll want to create this Policy in IAM first. In our example, we\u2019ll call the policy AllowExternalDNSUpdates (but you can call it whatever you prefer). If you prefer, you may fine-tune the policy to permit updates only to explicit Hosted Zone IDs. { \"Version\" : \"2012-10-17\" , \"Statement\" : [ { \"Effect\" : \"Allow\" , \"Action\" : [ \"route53:ChangeResourceRecordSets\" ], \"Resource\" : [ \"arn:aws:route53:::hostedzone/*\" ] }, { \"Effect\" : \"Allow\" , \"Action\" : [ \"route53:ListHostedZones\" , \"route53:ListResourceRecordSets\" , \"route53:ListTagsForResource\" ], \"Resource\" : [ \"*\" ] } ] } If you are using the AWS CLI, you can run the following to install the above policy (saved as policy.json ). This can be use in subsequent steps to allow ExternalDNS to access Route53 zones. aws iam create-policy --policy-name \"AllowExternalDNSUpdates\" --policy-document file://policy.json # example: arn:aws:iam::XXXXXXXXXXXX:policy/AllowExternalDNSUpdates export POLICY_ARN = $( aws iam list-policies \\ --query 'Policies[?PolicyName==`AllowExternalDNSUpdates`].Arn' --output text )","title":"IAM Policy"},{"location":"tutorials/aws/#provisioning-a-kubernetes-cluster","text":"You can use eksctl to easily provision an Amazon Elastic Kubernetes Service ( EKS ) cluster that is suitable for this tutorial. See Getting started with Amazon EKS \u2013 eksctl . export EKS_CLUSTER_NAME = \"my-externaldns-cluster\" export EKS_CLUSTER_REGION = \"us-east-2\" export KUBECONFIG = \" $HOME /.kube/ ${ EKS_CLUSTER_NAME } - ${ EKS_CLUSTER_REGION } .yaml\" eksctl create cluster --name $EKS_CLUSTER_NAME --region $EKS_CLUSTER_REGION Feel free to use other provisioning tools or an existing cluster. If Terraform is used, vpc and eks modules are recommended for standing up an EKS cluster. Amazon has a workshop called Amazon EKS Terraform Workshop that may be useful for this process.","title":"Provisioning a Kubernetes cluster"},{"location":"tutorials/aws/#permissions-to-modify-dns-zone","text":"You will need to use the above policy (represented by the POLICY_ARN environment variable) to allow ExternalDNS to update records in Route53 DNS zones. Here are three common ways this can be accomplished: Node IAM Role Static credentials IAM Roles for Service Accounts For this tutorial, ExternalDNS will use the environment variable EXTERNALDNS_NS to represent the namespace, defaulted to default . Feel free to change this to something else, such externaldns or kube-addons . Make sure to edit the subjects[0].namespace for the ClusterRoleBinding resource when deploying ExternalDNS with RBAC enabled. See Manifest (for clusters with RBAC enabled) for more information. Additionally, throughout this tutorial, the example domain of example.com is used. Change this to appropriate domain under your control. See Set up a hosted zone section.","title":"Permissions to modify DNS zone"},{"location":"tutorials/aws/#node-iam-role","text":"In this method, you can attach a policy to the Node IAM Role. This will allow nodes in the Kubernetes cluster to access Route53 zones, which allows ExternalDNS to update DNS records. Given that this allows all containers to access Route53, not just ExternalDNS, running on the node with these privileges, this method is not recommended, and is only suitable for limited limited test environments. If you are using eksctl to provision a new cluster, you add the policy at creation time with: eksctl create cluster --external-dns-access \\ --name $EKS_CLUSTER_NAME --region $EKS_CLUSTER_REGION \\ WARNING : This will assign allow read-write access to all nodes in the cluster, not just ExternalDNS. For this reason, this method is only suitable for limited test environments. If you already provisioned a cluster or use other provisioning tools like Terraform, you can use AWS CLI to attach the policy to the Node IAM Role.","title":"Node IAM Role"},{"location":"tutorials/aws/#get-the-node-iam-role-name","text":"The role name of the role associated with the node(s) where ExternalDNS will run is needed. An easy way to get the role name is to use the AWS web console (https://console.aws.amazon.com/eks/), and find any instance in the target node group and copy the role name associated with that instance.","title":"Get the Node IAM role name"},{"location":"tutorials/aws/#get-role-name-with-a-single-managed-nodegroup","text":"From the command line, if you have a single managed node group, the default with eksctl create cluster , you can find the role name with the following: # get managed node group name (assuming there's only one node group) GROUP_NAME = $( aws eks list-nodegroups --cluster-name $EKS_CLUSTER_NAME \\ --query nodegroups --out text ) # fetch role arn given node group name ROLE_ARN = $( aws eks describe-nodegroup --cluster-name $EKS_CLUSTER_NAME \\ --nodegroup-name $GROUP_NAME --query nodegroup.nodeRole --out text ) # extract just the name part of role arn ROLE_NAME = ${ NODE_ROLE_ARN ##*/ }","title":"Get role name with a single managed nodegroup"},{"location":"tutorials/aws/#get-role-name-with-other-configurations","text":"If you have multiple node groups or any unmanaged node groups, the process gets more complex. The first step is to get the instance host name of the desired node to where ExternalDNS will be deployed or is already deployed: # node instance name of one of the external dns pods currently running INSTANCE_NAME = $( kubectl get pods --all-namespaces \\ --selector app.kubernetes.io/instance = external-dns \\ --output jsonpath = '{.items[0].spec.nodeName}' ) # instance name of one of the nodes (change if node group is different) INSTANCE_NAME = $( kubectl get nodes --output name | cut -d '/' -f2 | tail -1 ) With the instance host name, you can then get the instance id: get_instance_id () { INSTANCE_NAME = $1 # example: ip-192-168-74-34.us-east-2.compute.internal # get list of nodes # ip-192-168-74-34.us-east-2.compute.internal aws:///us-east-2a/i-xxxxxxxxxxxxxxxxx # ip-192-168-86-105.us-east-2.compute.internal aws:///us-east-2a/i-xxxxxxxxxxxxxxxxx NODES = $( kubectl get nodes \\ --output jsonpath = '{range .items[*]}{.metadata.name}{\"\\t\"}{.spec.providerID}{\"\\n\"}{end}' ) # print instance id from matching node grep $INSTANCE_NAME <<< \" $NODES \" | cut -d '/' -f5 } INSTANCE_ID = $( get_instance_id $INSTANCE_NAME ) With the instance id, you can get the associated role name: findRoleName () { INSTANCE_ID = $1 # get all of the roles ROLES =( $( aws iam list-roles --query Roles [ * ] .RoleName --out text ) ) for ROLE in ${ ROLES [*] } ; do # get instance profile arn PROFILE_ARN = $( aws iam list-instance-profiles-for-role \\ --role-name $ROLE --query InstanceProfiles [ 0 ] .Arn --output text ) # if there is an instance profile if [[ \" $PROFILE_ARN \" ! = \"None\" ]] ; then # get all the instances with this associated instance profile INSTANCES = $( aws ec2 describe-instances \\ --filters Name = iam-instance-profile.arn,Values = $PROFILE_ARN \\ --query Reservations [ * ] .Instances [ 0 ] .InstanceId --out text ) # find instances that match the instant profile for INSTANCE in ${ INSTANCES [*] } ; do # set role name value if there is a match if [[ \" $INSTANCE_ID \" == \" $INSTANCE \" ]] ; then ROLE_NAME = $ROLE ; fi done fi done echo $ROLE_NAME } NODE_ROLE_NAME = $( findRoleName $INSTANCE_ID ) Using the role name, you can associate the policy that was created earlier: # attach policy arn created earlier to node IAM role aws iam attach-role-policy --role-name $NODE_ROLE_NAME --policy-arn $POLICY_ARN WARNING : This will assign allow read-write access to all pods running on the same node pool, not just the ExternalDNS pod(s).","title":"Get role name with other configurations"},{"location":"tutorials/aws/#deploy-externaldns-with-attached-policy-to-node-iam-role","text":"If ExternalDNS is not yet deployed, follow the steps under Deploy ExternalDNS using either RBAC or non-RBAC. NOTE : Before deleting the cluster during, be sure to run aws iam detach-role-policy . Otherwise, there can be errors as the provisioning system, such as eksctl or terraform , will not be able to delete the roles with the attached policy.","title":"Deploy ExternalDNS with attached policy to Node IAM Role"},{"location":"tutorials/aws/#static-credentials","text":"In this method, the policy is attached to an IAM user, and the credentials secrets for the IAM user are then made available using a Kubernetes secret. This method is not the preferred method as the secrets in the credential file could be copied and used by an unauthorized threat actor. However, if the Kubernetes cluster is not hosted on AWS, it may be the only method available. Given this situation, it is important to limit the associated privileges to just minimal required privileges, i.e. read-write access to Route53, and not used a credentials file that has extra privileges beyond what is required.","title":"Static credentials"},{"location":"tutorials/aws/#create-iam-user-and-attach-the-policy","text":"# create IAM user aws iam create-user --user-name \"externaldns\" # attach policy arn created earlier to IAM user aws iam attach-user-policy --user-name \"externaldns\" --policy-arn $POLICY_ARN","title":"Create IAM user and attach the policy"},{"location":"tutorials/aws/#create-the-static-credentials","text":"SECRET_ACCESS_KEY = $( aws iam create-access-key --user-name \"externaldns\" ) cat <<-EOF > /local/path/to/credentials [default] aws_access_key_id = $(echo $SECRET_ACCESS_KEY | jq -r '.AccessKey.AccessKeyId') aws_secret_access_key = $(echo $SECRET_ACCESS_KEY | jq -r '.AccessKey.SecretAccessKey') EOF","title":"Create the static credentials"},{"location":"tutorials/aws/#create-kubernetes-secret-from-credentials","text":"kubectl create secret generic external-dns \\ --namespace ${ EXTERNALDNS_NS :- \"default\" } --from-file /local/path/to/credentials","title":"Create Kubernetes secret from credentials"},{"location":"tutorials/aws/#deploy-externaldns-using-static-credentials","text":"Follow the steps under Deploy ExternalDNS using either RBAC or non-RBAC. Make sure to uncomment the section that mounts volumes, so that the credentials can be mounted.","title":"Deploy ExternalDNS using static credentials"},{"location":"tutorials/aws/#iam-roles-for-service-accounts","text":"IRSA ( IAM roles for Service Accounts ) allows cluster operators to map AWS IAM Roles to Kubernetes Service Accounts. This essentially allows only ExternalDNS pods to access Route53 without exposing any static credentials. This is the preferred method as it implements PoLP ( Principal of Least Privilege ). IMPORTANT : This method requires using KSA (Kuberntes service account) and RBAC. This method requires deploying with RBAC. See Manifest (for clusters with RBAC enabled) when ready to deploy ExternalDNS. NOTE : Similar methods to IRSA on AWS are kiam , which is in maintenence mode, and has instructions for creating an IAM role, and also kube2iam . IRSA is the officially supported method for EKS clusters, and so for non-EKS clusters on AWS, these other tools could be an option.","title":"IAM Roles for Service Accounts"},{"location":"tutorials/aws/#verify-oidc-is-supported","text":"aws eks describe-cluster --name $EKS_CLUSTER_NAME \\ --query \"cluster.identity.oidc.issuer\" --output text","title":"Verify OIDC is supported"},{"location":"tutorials/aws/#associate-oidc-to-cluster","text":"Configure the cluster with an OIDC provider and add support for IRSA ( IAM roles for Service Accounts ). If you used eksctl to provision the EKS cluster, you can update it with the following command: eksctl utils associate-iam-oidc-provider \\ --cluster $EKS_CLUSTER_NAME --approve If the cluster was provisioned with Terraform, you can use the iam_openid_connect_provider resource ( ref ) to associate to the OIDC provider.","title":"Associate OIDC to cluster"},{"location":"tutorials/aws/#create-an-iam-role-bound-to-a-service-account","text":"For the next steps in this process, we will need to associate the external-dns service account and a role used to grant access to Route53. This requires the following steps: Create a role with a trust relationship to the cluster\u2019s OIDC provider Attach the AllowExternalDNSUpdates policy to the role Create the external-dns service account Add annotation to the service account with the role arn","title":"Create an IAM role bound to a service account"},{"location":"tutorials/aws/#use-eksctl-with-eksctl-created-eks-cluster","text":"If eksctl was used to provision the EKS cluster, you can perform all of these steps with the following command: eksctl create iamserviceaccount \\ --cluster $EKS_CLUSTER_NAME \\ --name \"external-dns\" \\ --namespace ${ EXTERNALDNS_NS :- \"default\" } \\ --attach-policy-arn $POLICY_ARN \\ --approve","title":"Use eksctl with eksctl created EKS cluster"},{"location":"tutorials/aws/#use-aws-cli-with-any-eks-cluster","text":"Otherwise, we can do the following steps using aws commands (also see Creating an IAM role and policy for your service account ): ACCOUNT_ID = $( aws sts get-caller-identity \\ --query \"Account\" --output text ) OIDC_PROVIDER = $( aws eks describe-cluster --name $EKS_CLUSTER_NAME \\ --query \"cluster.identity.oidc.issuer\" --output text | sed -e 's|^https://||' ) cat <<-EOF > trust.json { \"Version\": \"2012-10-17\", \"Statement\": [ { \"Effect\": \"Allow\", \"Principal\": { \"Federated\": \"arn:aws:iam::$ACCOUNT_ID:oidc-provider/$OIDC_PROVIDER\" }, \"Action\": \"sts:AssumeRoleWithWebIdentity\", \"Condition\": { \"StringEquals\": { \"$OIDC_PROVIDER:sub\": \"system:serviceaccount:${EXTERNALDNS_NS:-\"default\"}:external-dns\", \"$OIDC_PROVIDER:aud\": \"sts.amazonaws.com\" } } } ] } EOF IRSA_ROLE = \"external-dns-irsa-role\" aws iam create-role --role-name $IRSA_ROLE --assume-role-policy-document file://trust.json aws iam attach-role-policy --role-name $IRSA_ROLE --policy-arn $POLICY_ARN ROLE_ARN = $( aws iam get-role --role-name $IRSA_ROLE --query Role.Arn --output text ) # Create service account (skip is already created) kubectl create serviceaccount \"external-dns\" --namespace ${ EXTERNALDNS_NS :- \"default\" } # Add annotation referencing IRSA role kubectl patch serviceaccount \"external-dns\" --namespace ${ EXTERNALDNS_NS :- \"default\" } --patch \\ \"{\\\"metadata\\\": { \\\"annotations\\\": { \\\"eks.amazonaws.com/role-arn\\\": \\\" $ROLE_ARN \\\" }}}\" If any part of this step is misconfigured, such as the role with incorrect namespace configured in the trust relationship, annotation pointing the the wrong role, etc., you will see errors like WebIdentityErr: failed to retrieve credentials . Check the configuration and make corrections. When the service account annotations are updated, then the current running pods will have to be terminated, so that new pod(s) with proper configuration (environment variables) will be created automatically. When annotation is added to service account, the ExternalDNS pod(s) scheduled will have AWS_ROLE_ARN , AWS_STS_REGIONAL_ENDPOINTS , and AWS_WEB_IDENTITY_TOKEN_FILE environment variables injected automatically.","title":"Use aws cli with any EKS cluster"},{"location":"tutorials/aws/#deploy-externaldns-using-irsa","text":"Follow the steps under Manifest (for clusters with RBAC enabled) . Make sure to comment out the service account section if this has been created already. If you deployed ExternalDNS before adding the service account annotation and the corresponding role, you will likely see error with failed to list hosted zones: AccessDenied: User . You can delete the current running ExternalDNS pod(s) after updating the annotation, so that new pods scheduled will have appropriate configuration to access Route53.","title":"Deploy ExternalDNS using IRSA"},{"location":"tutorials/aws/#set-up-a-hosted-zone","text":"If you prefer to try-out ExternalDNS in one of the existing hosted-zones you can skip this step Create a DNS zone which will contain the managed DNS records. This tutorial will use the fictional domain of example.com . aws route53 create-hosted-zone --name \"example.com.\" \\ --caller-reference \"external-dns-test- $( date +%s ) \" Make a note of the nameservers that were assigned to your new zone. ZONE_ID = $( aws route53 list-hosted-zones-by-name --output json \\ --dns-name \"example.com.\" --query HostedZones [ 0 ] .Id --out text ) aws route53 list-resource-record-sets --output text \\ --hosted-zone-id $ZONE_ID --query \\ \"ResourceRecordSets[?Type == 'NS'].ResourceRecords[*].Value | []\" | tr '\\t' '\\n' This should yield something similar this: ns-695.awsdns-22.net. ns-1313.awsdns-36.org. ns-350.awsdns-43.com. ns-1805.awsdns-33.co.uk. If using your own domain that was registered with a third-party domain registrar, you should point your domain\u2019s name servers to the values in the from the list above. Please consult your registrar\u2019s documentation on how to do that.","title":"Set up a hosted zone"},{"location":"tutorials/aws/#deploy-externaldns","text":"Connect your kubectl client to the cluster you want to test ExternalDNS with. Then apply one of the following manifests file to deploy ExternalDNS. You can check if your cluster has RBAC by kubectl api-versions | grep rbac.authorization.k8s.io . For clusters with RBAC enabled, be sure to choose the correct namespace . For this tutorial, the enviornment variable EXTERNALDNS_NS will refer to the namespace. You can set this to a value of your choice: export EXTERNALDNS_NS = \"default\" # externaldns, kube-addons, etc # create namespace if it does not yet exist kubectl get namespaces | grep -q $EXTERNALDNS_NS || \\ kubectl create namespace $EXTERNALDNS_NS","title":"Deploy ExternalDNS"},{"location":"tutorials/aws/#manifest-for-clusters-without-rbac-enabled","text":"Save the following below as externaldns-no-rbac.yaml . apiVersion : apps/v1 kind : Deployment metadata : name : external-dns labels : app.kubernetes.io/name : external-dns spec : strategy : type : Recreate selector : matchLabels : app.kubernetes.io/name : external-dns template : metadata : labels : app.kubernetes.io/name : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service - --source=ingress - --domain-filter=example.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones - --provider=aws - --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization - --aws-zone-type=public # only look at public hosted zones (valid values are public, private or no value for both) - --registry=txt - --txt-owner-id=my-hostedzone-identifier env : - name : AWS_DEFAULT_REGION value : us-east-1 # change to region where EKS is installed # # Uncomment below if using static credentials # - name: AWS_SHARED_CREDENTIALS_FILE # value: /.aws/credentials # volumeMounts: # - name: aws-credentials # mountPath: /.aws # readOnly: true # volumes: # - name: aws-credentials # secret: # secretName: external-dns When ready you can deploy: kubectl create --filename externaldns-no-rbac.yaml \\ --namespace ${ EXTERNALDNS_NS :- \"default\" }","title":"Manifest (for clusters without RBAC enabled)"},{"location":"tutorials/aws/#manifest-for-clusters-with-rbac-enabled","text":"Save the following below as externaldns-with-rbac.yaml . # comment out sa if it was previously created apiVersion : v1 kind : ServiceAccount metadata : name : external-dns labels : app.kubernetes.io/name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns labels : app.kubernetes.io/name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" , \"nodes\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer labels : app.kubernetes.io/name : external-dns roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default # change to desired namespace: externaldns, kube-addons --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns labels : app.kubernetes.io/name : external-dns spec : strategy : type : Recreate selector : matchLabels : app.kubernetes.io/name : external-dns template : metadata : labels : app.kubernetes.io/name : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service - --source=ingress - --domain-filter=example.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones - --provider=aws - --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization - --aws-zone-type=public # only look at public hosted zones (valid values are public, private or no value for both) - --registry=txt - --txt-owner-id=external-dns env : - name : AWS_DEFAULT_REGION value : us-east-1 # change to region where EKS is installed # # Uncommend below if using static credentials # - name: AWS_SHARED_CREDENTIALS_FILE # value: /.aws/credentials # volumeMounts: # - name: aws-credentials # mountPath: /.aws # readOnly: true # volumes: # - name: aws-credentials # secret: # secretName: external-dns When ready deploy: kubectl create --filename externaldns-with-rbac.yaml \\ --namespace ${ EXTERNALDNS_NS :- \"default\" }","title":"Manifest (for clusters with RBAC enabled)"},{"location":"tutorials/aws/#arguments","text":"This list is not the full list, but a few arguments that where chosen.","title":"Arguments"},{"location":"tutorials/aws/#aws-zone-type","text":"aws-zone-type allows filtering for private and public zones","title":"aws-zone-type"},{"location":"tutorials/aws/#annotations","text":"Annotations which are specific to AWS.","title":"Annotations"},{"location":"tutorials/aws/#alias","text":"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. To make the target an alias, the ingress needs to be configured correctly as described in the docs . In particular, the argument --publish-service=default/nginx-ingress-controller has to be set on the nginx-ingress-controller container. If one uses the nginx-ingress Helm chart, this flag can be set with the controller.publishService.enabled configuration option.","title":"alias"},{"location":"tutorials/aws/#target-hosted-zone","text":"external-dns.alpha.kubernetes.io/aws-target-hosted-zone can optionally be set to the ID of a Route53 hosted zone. This will force external-dns to use the specified hosted zone when creating an ALIAS target.","title":"target-hosted-zone"},{"location":"tutorials/aws/#verify-externaldns-works-service-example","text":"Create the following sample application to test that ExternalDNS works. For services ExternalDNS will look for the annotation external-dns.alpha.kubernetes.io/hostname on the service and use the corresponding value. If you want to give multiple names to service, you can set it to external-dns.alpha.kubernetes.io/hostname with a comma , separator. For this verification phase, you can use default or another namespace for the nginx demo, for example: NGINXDEMO_NS = \"nginx\" kubectl get namespaces | grep -q $NGINXDEMO_NS || kubectl create namespace $NGINXDEMO_NS Save the following manifest below as nginx.yaml : apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : nginx.example.com spec : type : LoadBalancer ports : - port : 80 name : http targetPort : 80 selector : app : nginx --- apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 name : http Deploy the nginx deployment and service with: kubectl create --filename nginx.yaml --namespace ${ NGINXDEMO_NS :- \"default\" } Verify that the load balancer was allocated with: kubectl get service nginx --namespace ${ NGINXDEMO_NS :- \"default\" } This should show something like: NAME TYPE CLUSTER-IP EXTERNAL-IP PORT ( S ) AGE nginx LoadBalancer 10 .100.47.41 ae11c2360188411e7951602725593fd1-1224345803.eu-central-1.elb.amazonaws.com. 80 :32749/TCP 12m After roughly two minutes check that a corresponding DNS record for your service that was created. aws route53 list-resource-record-sets --output json --hosted-zone-id $ZONE_ID \\ --query \"ResourceRecordSets[?Name == 'nginx.example.com.']|[?Type == 'A']\" This should show something like: [ { \"Name\" : \"nginx.example.com.\" , \"Type\" : \"A\" , \"AliasTarget\" : { \"HostedZoneId\" : \"ZEWFWZ4R16P7IB\" , \"DNSName\" : \"ae11c2360188411e7951602725593fd1-1224345803.eu-central-1.elb.amazonaws.com.\" , \"EvaluateTargetHealth\" : true } } ] You can also fetch the corresponding text records: aws route53 list-resource-record-sets --output json --hosted-zone-id $ZONE_ID \\ --query \"ResourceRecordSets[?Name == 'nginx.example.com.']|[?Type == 'TXT']\" This will show something like: [ { \"Name\" : \"nginx.example.com.\" , \"Type\" : \"TXT\" , \"TTL\" : 300 , \"ResourceRecords\" : [ { \"Value\" : \"\\\"heritage=external-dns,external-dns/owner=external-dns,external-dns/resource=service/default/nginx\\\"\" } ] } ] Note created TXT record alongside ALIAS record. TXT record signifies that the corresponding ALIAS record is managed by ExternalDNS. This makes ExternalDNS safe for running in environments where there are other records managed via other means. For more information about ALIAS record, see Choosing between alias and non-alias records . Let\u2019s check that we can resolve this DNS name. We\u2019ll ask the nameservers assigned to your zone first. dig +short @ns-5514.awsdns-53.org. nginx.example.com. This should return 1+ IP addresses that correspond to the ELB FQDN, i.e. ae11c2360188411e7951602725593fd1-1224345803.eu-central-1.elb.amazonaws.com. . Next try the public nameservers configured by DNS client on your system: dig +short nginx.example.com. If you hooked up your DNS zone with its parent zone correctly you can use curl to access your site. curl nginx.example.com. This should show something like: < html > < head > < title > Welcome to nginx! ... < body > < h1 > Welcome to nginx! ... ","title":"Verify ExternalDNS works (Service example)"},{"location":"tutorials/aws/#verify-externaldns-works-ingress-example","text":"With the previous deployment and service objects deployed, we can add an ingress object and configure a FQDN value for the host key. The ingress controller will match incoming HTTP traffic, and route it to the appropriate backend service based on the host key. For ingress objects ExternalDNS will create a DNS record based on the host specified for the ingress object. For this tutorial, we have two endpoints, the service with LoadBalancer type and an ingress. For practical purposes, if an ingress is used, the service type can be changed to ClusterIP as two endpoints are unecessary in this scenario. IMPORTANT : This requires that an ingress controller has been installed in your Kubernetes cluster. EKS does not come with an ingress controller by default. A popular ingress controller is ingress-nginx , which can be installed by a helm chart or by manifests . Create an ingress resource manifest file named ingress.yaml with the contents below: --- apiVersion : networking.k8s.io/v1 kind : Ingress metadata : name : nginx spec : ingressClassName : nginx rules : - host : server.example.com http : paths : - backend : service : name : nginx port : number : 80 path : / pathType : Prefix When ready, you can deploy this with: kubectl create --filename ingress.yaml --namespace ${ NGINXDEMO_NS :- \"default\" } Watch the status of the ingress until the ADDRESS field is populated. kubectl get ingress --watch --namespace ${ NGINXDEMO_NS :- \"default\" } You should see something like this: NAME CLASS HOSTS ADDRESS PORTS AGE nginx server.example.com 80 47s nginx server.example.com ae11c2360188411e7951602725593fd1-1224345803.eu-central-1.elb.amazonaws.com. 80 54s For the ingress test, run through similar checks, but using domain name used for the ingress: # check records on route53 aws route53 list-resource-record-sets --output json --hosted-zone-id $ZONE_ID \\ --query \"ResourceRecordSets[?Name == 'server.example.com.']\" # query using a route53 name server dig +short @ns-5514.awsdns-53.org. server.example.com. # query using the default name server dig +short server.example.com. # connect to the nginx web server through the ingress curl server.example.com.","title":"Verify ExternalDNS works (Ingress example)"},{"location":"tutorials/aws/#more-service-annotation-options","text":"","title":"More service annotation options"},{"location":"tutorials/aws/#custom-ttl","text":"The default DNS record TTL (Time-To-Live) is 300 seconds. You can customize this value by setting the annotation external-dns.alpha.kubernetes.io/ttl . e.g., modify the service manifest YAML file above: apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : nginx.example.com external-dns.alpha.kubernetes.io/ttl : \"60\" spec : ... This will set the DNS record\u2019s TTL to 60 seconds.","title":"Custom TTL"},{"location":"tutorials/aws/#routing-policies","text":"Route53 offers different routing policies . The routing policy for a record can be controlled with the following annotations: external-dns.alpha.kubernetes.io/set-identifier : this needs to be set to use any of the following routing policies For any given DNS name, only one of the following routing policies can be used: Weighted records: external-dns.alpha.kubernetes.io/aws-weight Latency-based routing: external-dns.alpha.kubernetes.io/aws-region Failover: external-dns.alpha.kubernetes.io/aws-failover Geolocation-based routing: external-dns.alpha.kubernetes.io/aws-geolocation-continent-code external-dns.alpha.kubernetes.io/aws-geolocation-country-code external-dns.alpha.kubernetes.io/aws-geolocation-subdivision-code Multi-value answer: external-dns.alpha.kubernetes.io/aws-multi-value-answer","title":"Routing policies"},{"location":"tutorials/aws/#associating-dns-records-with-healthchecks","text":"You can configure Route53 to associate DNS records with healthchecks for automated DNS failover using external-dns.alpha.kubernetes.io/aws-health-check-id: annotation. Note: ExternalDNS does not support creating healthchecks, and assumes that already exists.","title":"Associating DNS records with healthchecks"},{"location":"tutorials/aws/#canonical-hosted-zones","text":"When creating ALIAS type records in Route53 it is required that external-dns be aware of the canonical hosted zone in which the specified hostname is created. External-dns is able to automatically identify the canonical hosted zone for many hostnames based upon known hostname suffixes which are defined in aws.go . If a hostname does not have a known suffix then the suffix can be added into aws.go or the target-hosted-zone annotation can be used to manually define the ID of the canonical hosted zone.","title":"Canonical Hosted Zones"},{"location":"tutorials/aws/#govcloud-caveats","text":"Due to the special nature with how Route53 runs in Govcloud, there are a few tweaks in the deployment settings. An Environment variable with name of AWS_REGION set to either us-gov-west-1 or us-gov-east-1 is required. Otherwise it tries to lookup a region that does not exist in Govcloud and it errors out. env : - name : AWS_REGION value : us-gov-west-1 Route53 in Govcloud does not allow aliases. Therefore, container args must be set so that it uses CNAMES and a txt-prefix must be set to something. Otherwise, it will try to create a TXT record with the same value than the CNAME itself, which is not allowed. args : - --aws-prefer-cname - --txt-prefix={{ YOUR_PREFIX }} The first two changes are needed if you use Route53 in Govcloud, which only supports private zones. There are also no cross account IAM whatsoever between Govcloud and commercial AWS accounts. If services and ingresses need to make Route 53 entries to an public zone in a commercial account, you will have set env variables of AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY with a key and secret to the commercial account that has the sufficient rights. env : - name : AWS_ACCESS_KEY_ID value : XXXXXXXXX - name : AWS_SECRET_ACCESS_KEY valueFrom : secretKeyRef : name : {{ YOUR_SECRET_NAME }} key : {{ YOUR_SECRET_KEY }}","title":"Govcloud caveats"},{"location":"tutorials/aws/#clean-up","text":"Make sure to delete all Service objects before terminating the cluster so all load balancers get cleaned up correctly. kubectl delete service nginx IMPORTANT If you attached a policy to the Node IAM Role, then you will want to detach this before deleting the EKS cluster. Otherwise, the role resource will be locked, and the cluster cannot be deleted, especially if it was provisioned by automation like terraform or eksctl . aws iam detach-role-policy --role-name $NODE_ROLE_NAME --policy-arn $POLICY_ARN If the cluster was provisioned using eksctl , you can delete the cluster with: eksctl delete cluster --name $EKS_CLUSTER_NAME --region $EKS_CLUSTER_REGION Give ExternalDNS some time to clean up the DNS records for you. Then delete the hosted zone if you created one for the testing purpose. aws route53 delete-hosted-zone --id $NODE_ID # e.g /hostedzone/ZEWFWZ4R16P7IB If IAM user credentials were used, you can remove the user with: aws iam detach-user-policy --user-name \"externaldns\" --policy-arn $POLICY_ARN aws iam delete-user --user-name \"externaldns\" If IRSA was used, you can remove the IRSA role with: aws iam detach-role-policy --role-name $IRSA_ROLE --policy-arn $POLICY_ARN aws iam delete-role --role-name $IRSA_ROLE Delete any unneeded policies: aws iam delete-policy --policy-arn $POLICY_ARN","title":"Clean up"},{"location":"tutorials/aws/#throttling","text":"Route53 has a 5 API requests per second per account hard quota . Running several fast polling ExternalDNS instances in a given account can easily hit that limit. Some ways to reduce the request rate include: * Reduce the polling loop\u2019s synchronization interval at the possible cost of slower change propagation (but see --events below to reduce the impact). * --interval=5m (default 1m ) * Trigger the polling loop on changes to K8s objects, rather than only at interval , to have responsive updates with long poll intervals * --events * Limit the sources watched when the --events flag is specified to specific types, namespaces, labels, or annotations * --source=ingress --source=service - specify multiple times for multiple sources * --namespace=my-app * --label-filter=app in (my-app) * --ingress-class=nginx-external * Limit services watched by type (not applicable to ingress or other types) * --service-type-filter=LoadBalancer default all * Limit the hosted zones considered * --zone-id-filter=ABCDEF12345678 - specify multiple times if needed * --domain-filter=example.com by domain suffix - specify multiple times if needed * --regex-domain-filter=example* by domain suffix but as a regex - overrides domain-filter * --exclude-domains=ignore.this.example.com to exclude a domain or subdomain * --regex-domain-exclusion=ignore* subtracts it\u2019s matches from regex-domain-filter \u2019s matches * --aws-zone-type=public only sync zones of this type [public|private] * --aws-zone-tags=owner=k8s only sync zones with this tag * If the list of zones managed by ExternalDNS doesn\u2019t change frequently, cache it by setting a TTL. * --aws-zones-cache-duration=3h (default 0 - disabled) * Increase the number of changes applied to Route53 in each batch * --aws-batch-change-size=4000 (default 1000 ) * Increase the interval between changes * --aws-batch-change-interval=10s (default 1s ) * Introducing some jitter to the pod initialization, so that when multiple instances of ExternalDNS are updated at the same time they do not make their requests on the same second. A simple way to implement randomised startup is with an init container: ... spec: initContainers: - name: init-jitter image: registry.k8s.io/external-dns/external-dns:v0.13.5 command: - /bin/sh - -c - 'FOR=$((RANDOM % 10))s;echo \"Sleeping for $FOR\";sleep $FOR' containers: ...","title":"Throttling"},{"location":"tutorials/aws/#eks","text":"An effective starting point for EKS with an ingress controller might look like: --interval = 5m --events --source = ingress --domain-filter = example.com --aws-zones-cache-duration = 1h","title":"EKS"},{"location":"tutorials/azure-private-dns/","text":"Set up ExternalDNS for Azure Private DNS \u00b6 This tutorial describes how to set up ExternalDNS for managing records in Azure Private DNS. It comprises of the following steps: 1) Provision Azure Private DNS 2) Configure service principal for managing the zone 3) Deploy ExternalDNS 4) Expose an NGINX service with a LoadBalancer and annotate it with the desired DNS name 5) Install NGINX Ingress Controller (Optional) 6) Expose an nginx service with an ingress (Optional) 7) Verify the DNS records Everything will be deployed on Kubernetes. Therefore, please see the subsequent prerequisites. Prerequisites \u00b6 Azure Kubernetes Service is deployed and ready Azure CLI 2.0 and kubectl installed on the box to execute the subsequent steps Provision Azure Private DNS \u00b6 The provider will find suitable zones for domains it manages. It will not automatically create zones. For this tutorial, we will create a Azure resource group named \u2018externaldns\u2019 that can easily be deleted later. $ az group create -n externaldns -l westeurope Substitute a more suitable location for the resource group if desired. As a prerequisite for Azure Private DNS to resolve records is to define links with VNETs. Thus, first create a VNET. $ az network vnet create \\ --name myvnet \\ --resource-group externaldns \\ --location westeurope \\ --address-prefix 10.2.0.0/16 \\ --subnet-name mysubnet \\ --subnet-prefixes 10.2.0.0/24 Next, create a Azure Private DNS zone for \u201cexample.com\u201d: $ az network private-dns zone create -g externaldns -n example.com Substitute a domain you own for \u201cexample.com\u201d if desired. Finally, create the mentioned link with the VNET. $ az network private-dns link vnet create -g externaldns -n mylink \\ -z example.com -v myvnet --registration-enabled false Configure service principal for managing the zone \u00b6 ExternalDNS needs permissions to make changes in Azure Private DNS. These permissions are roles assigned to the service principal used by ExternalDNS. A service principal with a minimum access level of Private DNS Zone Contributor to the Private DNS zone(s) and Reader to the resource group containing the Azure Private DNS zone(s) is necessary. More powerful role-assignments like Owner or assignments on subscription-level work too. Start off by creating the service principal without role-assignments. $ az ad sp create-for-rbac --skip-assignment -n http://externaldns-sp { \"appId\": \"appId GUID\", <-- aadClientId value ... \"password\": \"password\", <-- aadClientSecret value \"tenant\": \"AzureAD Tenant Id\" <-- tenantId value } Note: Alternatively, you can issue az account show --query \"tenantId\" to retrieve the id of your AAD Tenant too. Next, assign the roles to the service principal. But first retrieve the ID\u2019s of the objects to assign roles on. # find out the resource ids of the resource group where the dns zone is deployed, and the dns zone itself $ az group show --name externaldns --query id -o tsv /subscriptions/id/resourceGroups/externaldns $ az network private-dns zone show --name example.com -g externaldns --query id -o tsv /subscriptions/.../resourceGroups/externaldns/providers/Microsoft.Network/privateDnsZones/example.com Now, create role assignments . # 1. as a reader to the resource group $ az role assignment create --role \"Reader\" --assignee --scope # 2. as a contributor to DNS Zone itself $ az role assignment create --role \"Private DNS Zone Contributor\" --assignee --scope Deploy ExternalDNS \u00b6 Configure kubectl to be able to communicate and authenticate with your cluster. This is per default done through the file ~/.kube/config . For general background information on this see kubernetes-docs . Azure-CLI features functionality for automatically maintaining this file for AKS-Clusters. See Azure-Docs . Follow the steps for azure-dns provider to create a configuration file. Then apply one of the following manifests depending on whether you use RBAC or not. The credentials of the service principal are provided to ExternalDNS as environment-variables. Manifest (for clusters without RBAC enabled) \u00b6 apiVersion : apps/v1 kind : Deployment metadata : name : externaldns spec : selector : matchLabels : app : externaldns strategy : type : Recreate template : metadata : labels : app : externaldns spec : containers : - name : externaldns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service - --source=ingress - --domain-filter=example.com - --provider=azure-private-dns - --azure-resource-group=externaldns - --azure-subscription-id= volumeMounts : - name : azure-config-file mountPath : /etc/kubernetes readOnly : true volumes : - name : azure-config-file secret : secretName : azure-config-file Manifest (for clusters with RBAC enabled, cluster access) \u00b6 apiVersion : v1 kind : ServiceAccount metadata : name : externaldns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : externaldns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : externaldns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : externaldns subjects : - kind : ServiceAccount name : externaldns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : externaldns spec : selector : matchLabels : app : externaldns strategy : type : Recreate template : metadata : labels : app : externaldns spec : serviceAccountName : externaldns containers : - name : externaldns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service - --source=ingress - --domain-filter=example.com - --provider=azure-private-dns - --azure-resource-group=externaldns - --azure-subscription-id= volumeMounts : - name : azure-config-file mountPath : /etc/kubernetes readOnly : true volumes : - name : azure-config-file secret : secretName : azure-config-file Manifest (for clusters with RBAC enabled, namespace access) \u00b6 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 requires cluster access, so when using this manifest, services with type NodePort will be skipped! apiVersion : v1 kind : ServiceAccount metadata : name : externaldns --- apiVersion : rbac.authorization.k8s.io/v1 kind : Role metadata : name : externaldns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : RoleBinding metadata : name : externaldns roleRef : apiGroup : rbac.authorization.k8s.io kind : Role name : externaldns subjects : - kind : ServiceAccount name : externaldns --- apiVersion : apps/v1 kind : Deployment metadata : name : externaldns spec : selector : matchLabels : app : externaldns strategy : type : Recreate template : metadata : labels : app : externaldns spec : serviceAccountName : externaldns containers : - name : externaldns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service - --source=ingress - --domain-filter=example.com - --provider=azure-private-dns - --azure-resource-group=externaldns - --azure-subscription-id= 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: $ kubectl create -f externaldns.yaml Create an nginx deployment \u00b6 This step creates a demo workload in your cluster. Apply the following manifest to create a deployment that we are going to expose later in this tutorial in multiple ways: --- apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 Expose the nginx deployment with a load balancer \u00b6 Apply the following manifest to create a service of type LoadBalancer . This will create a public load balancer in Azure that will forward traffic to the nginx pods. --- apiVersion : v1 kind : Service annotations : service.beta.kubernetes.io/azure-load-balancer-internal : \"true\" external-dns.alpha.kubernetes.io/hostname : server.example.com external-dns.alpha.kubernetes.io/internal-hostname : server-clusterip.example.com metadata : name : nginx-svc spec : ports : - port : 80 protocol : TCP targetPort : 80 selector : app : nginx type : LoadBalancer In the service we used multiple annptations. The annotation service.beta.kubernetes.io/azure-load-balancer-internal is used to create an internal load balancer. The annotation external-dns.alpha.kubernetes.io/hostname is used to create a DNS record for the load balancer that will point to the internal IP address in the VNET allocated by the internal load balancer. The annotation external-dns.alpha.kubernetes.io/internal-hostname is used to create a private DNS record for the load balancer that will point to the cluster IP. Install NGINX Ingress Controller (Optional) \u00b6 Helm is used to deploy the ingress controller. We employ the popular chart ingress-nginx . $ helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx $ helm repo update $ helm install [RELEASE_NAME] ingress-nginx/ingress-nginx --set controller.publishService.enabled=true The parameter controller.publishService.enabled needs to be set to true. It will make the ingress controller update the endpoint records of ingress-resources to contain the external-ip of the loadbalancer serving the ingress-controller. This is crucial as ExternalDNS reads those endpoints records when creating DNS-Records from ingress-resources. In the subsequent parameter we will make use of this. If you don\u2019t want to work with ingress-resources in your later use, you can leave the parameter out. Verify the correct propagation of the loadbalancer\u2019s ip by listing the ingresses. $ kubectl get ingress The address column should contain the ip for each ingress. ExternalDNS will pick up exactly this piece of information. NAME HOSTS ADDRESS PORTS AGE nginx1 sample1.aks.com 52.167.195.110 80 6d22h nginx2 sample2.aks.com 52.167.195.110 80 6d21h If you do not want to deploy the ingress controller with Helm, ensure to pass the following cmdline-flags to it through the mechanism of your choice: flags: --publish-service=/ --update-status=true (default-value) example: ./nginx-ingress-controller --publish-service=default/nginx-ingress-controller Expose the nginx deployment with the ingress (Optional) \u00b6 Apply the following manifest to create an ingress resource that will expose the nginx deployment. The ingress resource backend points to a ClusterIP service that is needed to select the pods that will receive the traffic. --- apiVersion : v1 kind : Service metadata : name : nginx-svc-clusterip spec : ports : - port : 80 protocol : TCP targetPort : 80 selector : app : nginx type : ClusterIP --- apiVersion : networking.k8s.io/v1 kind : Ingress metadata : name : nginx spec : ingressClassName : nginx rules : - host : server.example.com http : paths : - backend : service : name : nginx-svc-clusterip port : number : 80 pathType : Prefix When using ExternalDNS with ingress objects it will automatically create DNS records based on host names specified in ingress objects that match the domain-filter argument in the externaldns deployment manifest. When those host names are removed or renamed the corresponding DNS records are also altered. Create the deployment, service and ingress object: $ kubectl create -f nginx.yaml Since your external IP would have already been assigned to the nginx-ingress service, the DNS records pointing to the IP of the nginx-ingress service should be created within a minute. Verify created records \u00b6 Run the following command to view the A records for your Azure Private DNS zone: $ az network private-dns record-set a list -g externaldns -z example.com Substitute the zone for the one created above if a different domain was used. This should show the external IP address of the service as the A record for your domain (\u2018@\u2019 indicates the record is for the zone itself).","title":"Set up ExternalDNS for Azure Private DNS"},{"location":"tutorials/azure-private-dns/#set-up-externaldns-for-azure-private-dns","text":"This tutorial describes how to set up ExternalDNS for managing records in Azure Private DNS. It comprises of the following steps: 1) Provision Azure Private DNS 2) Configure service principal for managing the zone 3) Deploy ExternalDNS 4) Expose an NGINX service with a LoadBalancer and annotate it with the desired DNS name 5) Install NGINX Ingress Controller (Optional) 6) Expose an nginx service with an ingress (Optional) 7) Verify the DNS records Everything will be deployed on Kubernetes. Therefore, please see the subsequent prerequisites.","title":"Set up ExternalDNS for Azure Private DNS"},{"location":"tutorials/azure-private-dns/#prerequisites","text":"Azure Kubernetes Service is deployed and ready Azure CLI 2.0 and kubectl installed on the box to execute the subsequent steps","title":"Prerequisites"},{"location":"tutorials/azure-private-dns/#provision-azure-private-dns","text":"The provider will find suitable zones for domains it manages. It will not automatically create zones. For this tutorial, we will create a Azure resource group named \u2018externaldns\u2019 that can easily be deleted later. $ az group create -n externaldns -l westeurope Substitute a more suitable location for the resource group if desired. As a prerequisite for Azure Private DNS to resolve records is to define links with VNETs. Thus, first create a VNET. $ az network vnet create \\ --name myvnet \\ --resource-group externaldns \\ --location westeurope \\ --address-prefix 10.2.0.0/16 \\ --subnet-name mysubnet \\ --subnet-prefixes 10.2.0.0/24 Next, create a Azure Private DNS zone for \u201cexample.com\u201d: $ az network private-dns zone create -g externaldns -n example.com Substitute a domain you own for \u201cexample.com\u201d if desired. Finally, create the mentioned link with the VNET. $ az network private-dns link vnet create -g externaldns -n mylink \\ -z example.com -v myvnet --registration-enabled false","title":"Provision Azure Private DNS"},{"location":"tutorials/azure-private-dns/#configure-service-principal-for-managing-the-zone","text":"ExternalDNS needs permissions to make changes in Azure Private DNS. These permissions are roles assigned to the service principal used by ExternalDNS. A service principal with a minimum access level of Private DNS Zone Contributor to the Private DNS zone(s) and Reader to the resource group containing the Azure Private DNS zone(s) is necessary. More powerful role-assignments like Owner or assignments on subscription-level work too. Start off by creating the service principal without role-assignments. $ az ad sp create-for-rbac --skip-assignment -n http://externaldns-sp { \"appId\": \"appId GUID\", <-- aadClientId value ... \"password\": \"password\", <-- aadClientSecret value \"tenant\": \"AzureAD Tenant Id\" <-- tenantId value } Note: Alternatively, you can issue az account show --query \"tenantId\" to retrieve the id of your AAD Tenant too. Next, assign the roles to the service principal. But first retrieve the ID\u2019s of the objects to assign roles on. # find out the resource ids of the resource group where the dns zone is deployed, and the dns zone itself $ az group show --name externaldns --query id -o tsv /subscriptions/id/resourceGroups/externaldns $ az network private-dns zone show --name example.com -g externaldns --query id -o tsv /subscriptions/.../resourceGroups/externaldns/providers/Microsoft.Network/privateDnsZones/example.com Now, create role assignments . # 1. as a reader to the resource group $ az role assignment create --role \"Reader\" --assignee --scope # 2. as a contributor to DNS Zone itself $ az role assignment create --role \"Private DNS Zone Contributor\" --assignee --scope ","title":"Configure service principal for managing the zone"},{"location":"tutorials/azure-private-dns/#deploy-externaldns","text":"Configure kubectl to be able to communicate and authenticate with your cluster. This is per default done through the file ~/.kube/config . For general background information on this see kubernetes-docs . Azure-CLI features functionality for automatically maintaining this file for AKS-Clusters. See Azure-Docs . Follow the steps for azure-dns provider to create a configuration file. Then apply one of the following manifests depending on whether you use RBAC or not. The credentials of the service principal are provided to ExternalDNS as environment-variables.","title":"Deploy ExternalDNS"},{"location":"tutorials/azure-private-dns/#manifest-for-clusters-without-rbac-enabled","text":"apiVersion : apps/v1 kind : Deployment metadata : name : externaldns spec : selector : matchLabels : app : externaldns strategy : type : Recreate template : metadata : labels : app : externaldns spec : containers : - name : externaldns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service - --source=ingress - --domain-filter=example.com - --provider=azure-private-dns - --azure-resource-group=externaldns - --azure-subscription-id= volumeMounts : - name : azure-config-file mountPath : /etc/kubernetes readOnly : true volumes : - name : azure-config-file secret : secretName : azure-config-file","title":"Manifest (for clusters without RBAC enabled)"},{"location":"tutorials/azure-private-dns/#manifest-for-clusters-with-rbac-enabled-cluster-access","text":"apiVersion : v1 kind : ServiceAccount metadata : name : externaldns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : externaldns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : externaldns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : externaldns subjects : - kind : ServiceAccount name : externaldns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : externaldns spec : selector : matchLabels : app : externaldns strategy : type : Recreate template : metadata : labels : app : externaldns spec : serviceAccountName : externaldns containers : - name : externaldns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service - --source=ingress - --domain-filter=example.com - --provider=azure-private-dns - --azure-resource-group=externaldns - --azure-subscription-id= volumeMounts : - name : azure-config-file mountPath : /etc/kubernetes readOnly : true volumes : - name : azure-config-file secret : secretName : azure-config-file","title":"Manifest (for clusters with RBAC enabled, cluster access)"},{"location":"tutorials/azure-private-dns/#manifest-for-clusters-with-rbac-enabled-namespace-access","text":"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 requires cluster access, so when using this manifest, services with type NodePort will be skipped! apiVersion : v1 kind : ServiceAccount metadata : name : externaldns --- apiVersion : rbac.authorization.k8s.io/v1 kind : Role metadata : name : externaldns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : RoleBinding metadata : name : externaldns roleRef : apiGroup : rbac.authorization.k8s.io kind : Role name : externaldns subjects : - kind : ServiceAccount name : externaldns --- apiVersion : apps/v1 kind : Deployment metadata : name : externaldns spec : selector : matchLabels : app : externaldns strategy : type : Recreate template : metadata : labels : app : externaldns spec : serviceAccountName : externaldns containers : - name : externaldns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service - --source=ingress - --domain-filter=example.com - --provider=azure-private-dns - --azure-resource-group=externaldns - --azure-subscription-id= 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: $ kubectl create -f externaldns.yaml","title":"Manifest (for clusters with RBAC enabled, namespace access)"},{"location":"tutorials/azure-private-dns/#create-an-nginx-deployment","text":"This step creates a demo workload in your cluster. Apply the following manifest to create a deployment that we are going to expose later in this tutorial in multiple ways: --- apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80","title":"Create an nginx deployment"},{"location":"tutorials/azure-private-dns/#expose-the-nginx-deployment-with-a-load-balancer","text":"Apply the following manifest to create a service of type LoadBalancer . This will create a public load balancer in Azure that will forward traffic to the nginx pods. --- apiVersion : v1 kind : Service annotations : service.beta.kubernetes.io/azure-load-balancer-internal : \"true\" external-dns.alpha.kubernetes.io/hostname : server.example.com external-dns.alpha.kubernetes.io/internal-hostname : server-clusterip.example.com metadata : name : nginx-svc spec : ports : - port : 80 protocol : TCP targetPort : 80 selector : app : nginx type : LoadBalancer In the service we used multiple annptations. The annotation service.beta.kubernetes.io/azure-load-balancer-internal is used to create an internal load balancer. The annotation external-dns.alpha.kubernetes.io/hostname is used to create a DNS record for the load balancer that will point to the internal IP address in the VNET allocated by the internal load balancer. The annotation external-dns.alpha.kubernetes.io/internal-hostname is used to create a private DNS record for the load balancer that will point to the cluster IP.","title":"Expose the nginx deployment with a load balancer"},{"location":"tutorials/azure-private-dns/#install-nginx-ingress-controller-optional","text":"Helm is used to deploy the ingress controller. We employ the popular chart ingress-nginx . $ helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx $ helm repo update $ helm install [RELEASE_NAME] ingress-nginx/ingress-nginx --set controller.publishService.enabled=true The parameter controller.publishService.enabled needs to be set to true. It will make the ingress controller update the endpoint records of ingress-resources to contain the external-ip of the loadbalancer serving the ingress-controller. This is crucial as ExternalDNS reads those endpoints records when creating DNS-Records from ingress-resources. In the subsequent parameter we will make use of this. If you don\u2019t want to work with ingress-resources in your later use, you can leave the parameter out. Verify the correct propagation of the loadbalancer\u2019s ip by listing the ingresses. $ kubectl get ingress The address column should contain the ip for each ingress. ExternalDNS will pick up exactly this piece of information. NAME HOSTS ADDRESS PORTS AGE nginx1 sample1.aks.com 52.167.195.110 80 6d22h nginx2 sample2.aks.com 52.167.195.110 80 6d21h If you do not want to deploy the ingress controller with Helm, ensure to pass the following cmdline-flags to it through the mechanism of your choice: flags: --publish-service=/ --update-status=true (default-value) example: ./nginx-ingress-controller --publish-service=default/nginx-ingress-controller","title":"Install NGINX Ingress Controller (Optional)"},{"location":"tutorials/azure-private-dns/#expose-the-nginx-deployment-with-the-ingress-optional","text":"Apply the following manifest to create an ingress resource that will expose the nginx deployment. The ingress resource backend points to a ClusterIP service that is needed to select the pods that will receive the traffic. --- apiVersion : v1 kind : Service metadata : name : nginx-svc-clusterip spec : ports : - port : 80 protocol : TCP targetPort : 80 selector : app : nginx type : ClusterIP --- apiVersion : networking.k8s.io/v1 kind : Ingress metadata : name : nginx spec : ingressClassName : nginx rules : - host : server.example.com http : paths : - backend : service : name : nginx-svc-clusterip port : number : 80 pathType : Prefix When using ExternalDNS with ingress objects it will automatically create DNS records based on host names specified in ingress objects that match the domain-filter argument in the externaldns deployment manifest. When those host names are removed or renamed the corresponding DNS records are also altered. Create the deployment, service and ingress object: $ kubectl create -f nginx.yaml Since your external IP would have already been assigned to the nginx-ingress service, the DNS records pointing to the IP of the nginx-ingress service should be created within a minute.","title":"Expose the nginx deployment with the ingress (Optional)"},{"location":"tutorials/azure-private-dns/#verify-created-records","text":"Run the following command to view the A records for your Azure Private DNS zone: $ az network private-dns record-set a list -g externaldns -z example.com Substitute the zone for the one created above if a different domain was used. This should show the external IP address of the service as the A record for your domain (\u2018@\u2019 indicates the record is for the zone itself).","title":"Verify created records"},{"location":"tutorials/azure/","text":"Setting up ExternalDNS for Services on Azure \u00b6 This tutorial describes how to setup ExternalDNS for Azure DNS with Azure Kubernetes Service . Make sure to use >=0.11.0 version of ExternalDNS for this tutorial. This tutorial uses Azure CLI 2.0 for all Azure commands and assumes that the Kubernetes cluster was created via Azure Container Services and kubectl commands are being run on an orchestration node. Creating an Azure DNS zone \u00b6 The Azure provider for ExternalDNS will find suitable zones for domains it manages; it will not automatically create zones. For this tutorial, we will create a Azure resource group named MyDnsResourceGroup that can easily be deleted later: $ az group create --name \"MyDnsResourceGroup\" --location \"eastus\" Substitute a more suitable location for the resource group if desired. Next, create a Azure DNS zone for example.com : $ az network dns zone create --resource-group \"MyDnsResourceGroup\" --name \"example.com\" Substitute a domain you own for example.com if desired. If using your own domain that was registered with a third-party domain registrar, you should point your domain\u2019s name servers to the values in the nameServers field from the JSON data returned by the az network dns zone create command. Please consult your registrar\u2019s documentation on how to do that. Configuration file \u00b6 The azure provider will reference a configuration file called azure.json . The preferred way to inject the configuration file is by using a Kubernetes secret. The secret should contain an object named azure.json with content similar to this: { \"tenantId\" : \"01234abc-de56-ff78-abc1-234567890def\" , \"subscriptionId\" : \"01234abc-de56-ff78-abc1-234567890def\" , \"resourceGroup\" : \"MyDnsResourceGroup\" , \"aadClientId\" : \"01234abc-de56-ff78-abc1-234567890def\" , \"aadClientSecret\" : \"uKiuXeiwui4jo9quae9o\" } The following fields are used: tenantId ( required ) - run az account show --query \"tenantId\" or by selecting Azure Active Directory in the Azure Portal and checking the Directory ID under Properties. subscriptionId ( required ) - run az account show --query \"id\" or by selecting Subscriptions in the Azure Portal. resourceGroup ( required ) is the Resource Group created in a previous step that contains the Azure DNS Zone. aadClientID and aadClientSecret are associated with the Service Principal. This is only used with Service Principal method documented in the next section. useManagedIdentityExtension - this is set to true if you use either AKS Kubelet Identity or AAD Pod Identities methods documented in the next section. userAssignedIdentityID - this contains the client id from the Managed identitty when using the AAD Pod Identities method documented in the next setion. useWorkloadIdentityExtension - this is set to true if you use Workload Identity method documented in the next section. The Azure DNS provider expects, by default, that the configuration file is at /etc/kubernetes/azure.json . This can be overridden with the --azure-config-file option when starting ExternalDNS. Permissions to modify DNS zone \u00b6 ExternalDNS needs permissions to make changes to the Azure DNS zone. There are three ways configure the access needed: Service Principal Managed Identity Using AKS Kubelet Identity Managed Identity Using AAD Pod Identities Managed Identity Using Workload Identity Service Principal \u00b6 These permissions are defined in a Service Principal that should be made available to ExternalDNS as a configuration file azure.json . Creating a service principal \u00b6 A Service Principal with a minimum access level of DNS Zone Contributor or Contributor to the DNS zone(s) and Reader to the resource group containing the Azure DNS zone(s) is necessary for ExternalDNS to be able to edit DNS records. However, other more permissive access levels will work too (e.g. Contributor to the resource group or the whole subscription). This is an Azure CLI example on how to query the Azure API for the information required for the Resource Group and DNS zone you would have already created in previous steps (requires azure-cli and jq ) $ EXTERNALDNS_NEW_SP_NAME = \"ExternalDnsServicePrincipal\" # name of the service principal $ AZURE_DNS_ZONE_RESOURCE_GROUP = \"MyDnsResourceGroup\" # name of resource group where dns zone is hosted $ AZURE_DNS_ZONE = \"example.com\" # DNS zone name like example.com or sub.example.com # Create the service principal $ DNS_SP = $( az ad sp create-for-rbac --name $EXTERNALDNS_NEW_SP_NAME ) $ EXTERNALDNS_SP_APP_ID = $( echo $DNS_SP | jq -r '.appId' ) $ EXTERNALDNS_SP_PASSWORD = $( echo $DNS_SP | jq -r '.password' ) Assign the rights for the service principal \u00b6 Grant access to Azure DNS zone for the service principal. # fetch DNS id used to grant access to the service principal DNS_ID = $( az network dns zone show --name $AZURE_DNS_ZONE \\ --resource-group $AZURE_DNS_ZONE_RESOURCE_GROUP --query \"id\" --output tsv ) # 1. as a reader to the resource group $ az role assignment create --role \"Reader\" --assignee $EXTERNALDNS_SP_APP_ID --scope $DNS_ID # 2. as a contributor to DNS Zone itself $ az role assignment create --role \"Contributor\" --assignee $EXTERNALDNS_SP_APP_ID --scope $DNS_ID Creating a configuration file for the service principal \u00b6 Create the file azure.json with values gather from previous steps. cat <<-EOF > /local/path/to/azure.json { \"tenantId\": \"$(az account show --query tenantId -o tsv)\", \"subscriptionId\": \"$(az account show --query id -o tsv)\", \"resourceGroup\": \"$AZURE_DNS_ZONE_RESOURCE_GROUP\", \"aadClientId\": \"$EXTERNALDNS_SP_APP_ID\", \"aadClientSecret\": \"$EXTERNALDNS_SP_PASSWORD\" } EOF Use this file to create a Kubernetes secret: $ kubectl create secret generic azure-config-file --namespace \"default\" --from-file /local/path/to/azure.json Managed identity using AKS Kubelet identity \u00b6 The managed identity that is assigned to the underlying node pool in the AKS cluster can be given permissions to access Azure DNS. Managed identities are essentially a service principal whose lifecycle is managed, such as deleting the AKS cluster will also delete the service principals associated with the AKS cluster. The managed identity assigned Kuberetes node pool, or specifically the VMSS , is called the Kubelet identity. The managed identites were previously called MSI (Managed Service Identity) and are enabled by default when creating an AKS cluster. Note that permissions granted to this identity will be accessible to all containers running inside the Kubernetes cluster, not just the ExternalDNS container(s). For the managed identity, the contents of azure.json should be similar to this: { \"tenantId\" : \"01234abc-de56-ff78-abc1-234567890def\" , \"subscriptionId\" : \"01234abc-de56-ff78-abc1-234567890def\" , \"resourceGroup\" : \"MyDnsResourceGroup\" , \"useManagedIdentityExtension\" : true } Fetching the Kubelet identity \u00b6 For this process, you will need to get the kublet identity: $ PRINCIPAL_ID = $( az aks show --resource-group $CLUSTER_GROUP --name $CLUSTERNAME \\ --query \"identityProfile.kubeletidentity.objectId\" --output tsv ) Assign rights for the Kubelet identity \u00b6 Grant access to Azure DNS zone for the kublet identity. $ AZURE_DNS_ZONE = \"example.com\" # DNS zone name like example.com or sub.example.com $ AZURE_DNS_ZONE_RESOURCE_GROUP = \"MyDnsResourceGroup\" # resource group where DNS zone is hosted # fetch DNS id used to grant access to the kublet identity $ DNS_ID = $( az network dns zone show --name $AZURE_DNS_ZONE \\ --resource-group $AZURE_DNS_ZONE_RESOURCE_GROUP --query \"id\" --output tsv ) $ az role assignment create --role \"DNS Zone Contributor\" --assignee $PRINCIPAL_ID --scope $DNS_ID Creating a configuration file for the kubelet identity \u00b6 Create the file azure.json with values gather from previous steps. cat <<-EOF > /local/path/to/azure.json { \"tenantId\": \"$(az account show --query tenantId -o tsv)\", \"subscriptionId\": \"$(az account show --query id -o tsv)\", \"resourceGroup\": \"$AZURE_DNS_ZONE_RESOURCE_GROUP\", \"useManagedIdentityExtension\": true } EOF Use the azure.json file to create a Kubernetes secret: $ kubectl create secret generic azure-config-file --namespace \"default\" --from-file /local/path/to/azure.json Managed identity using AAD Pod Identities \u00b6 For this process, we will create a managed identity that will be explicitly used by the ExternalDNS container. This process is similar to Kubelet identity except that this managed identity is not associated with the Kubernetes node pool, but rather associated with explicit ExternalDNS containers. Enable the AAD Pod Identities feature \u00b6 For this solution, AAD Pod Identities preview feature can be enabled. The commands below should do the trick to enable this feature: $ az feature register --name EnablePodIdentityPreview --namespace Microsoft.ContainerService $ az feature register --name AutoUpgradePreview --namespace Microsoft.ContainerService $ az extension add --name aks-preview $ az extension update --name aks-preview $ az provider register --namespace Microsoft.ContainerService Deploy the AAD Pod Identities service \u00b6 Once enabled, you can update your cluster and install needed services for the AAD Pod Identities feature. $ AZURE_AKS_RESOURCE_GROUP = \"my-aks-cluster-group\" # name of resource group where aks cluster was created $ AZURE_AKS_CLUSTER_NAME = \"my-aks-cluster\" # name of aks cluster previously created $ az aks update --resource-group ${ AZURE_AKS_RESOURCE_GROUP } --name ${ AZURE_AKS_CLUSTER_NAME } --enable-pod-identity Note that, if you use the default network plugin kubenet , then you need to add the command line option --enable-pod-identity-with-kubenet to the above command. Creating the managed identity \u00b6 After this process is finished, create a managed identity. $ IDENTITY_RESOURCE_GROUP = $AZURE_AKS_RESOURCE_GROUP # custom group or reuse AKS group $ IDENTITY_NAME = \"example-com-identity\" # create a managed identity $ az identity create --resource-group \" ${ IDENTITY_RESOURCE_GROUP } \" --name \" ${ IDENTITY_NAME } \" Assign rights for the managed identity \u00b6 Grant access to Azure DNS zone for the managed identity. $ AZURE_DNS_ZONE_RESOURCE_GROUP = \"MyDnsResourceGroup\" # name of resource group where dns zone is hosted $ AZURE_DNS_ZONE = \"example.com\" # DNS zone name like example.com or sub.example.com # fetch identity client id from managed identity created earlier $ IDENTITY_CLIENT_ID = $( az identity show --resource-group \" ${ IDENTITY_RESOURCE_GROUP } \" \\ --name \" ${ IDENTITY_NAME } \" --query \"clientId\" --output tsv ) # fetch DNS id used to grant access to the managed identity $ DNS_ID = $( az network dns zone show --name \" ${ AZURE_DNS_ZONE } \" \\ --resource-group \" ${ AZURE_DNS_ZONE_RESOURCE_GROUP } \" --query \"id\" --output tsv ) $ az role assignment create --role \"DNS Zone Contributor\" \\ --assignee \" ${ IDENTITY_CLIENT_ID } \" --scope \" ${ DNS_ID } \" Creating a configuration file for the managed identity \u00b6 Create the file azure.json with the values from previous steps: cat <<-EOF > /local/path/to/azure.json { \"tenantId\": \"$(az account show --query tenantId -o tsv)\", \"subscriptionId\": \"$(az account show --query id -o tsv)\", \"resourceGroup\": \"$AZURE_DNS_ZONE_RESOURCE_GROUP\", \"useManagedIdentityExtension\": true, \"userAssignedIdentityID\": \"$IDENTITY_CLIENT_ID\" } EOF Use the azure.json file to create a Kubernetes secret: $ kubectl create secret generic azure-config-file --namespace \"default\" --from-file /local/path/to/azure.json Creating an Azure identity binding \u00b6 A binding between the managed identity and the ExternalDNS pods needs to be setup by creating AzureIdentity and AzureIdentityBinding resources. This will allow appropriately labeled ExternalDNS pods to authenticate using the managed identity. When AAD Pod Identity feature is enabled from previous steps above, the az aks pod-identity add can be used to create these resources: $ IDENTITY_RESOURCE_ID = $( az identity show --resource-group ${ IDENTITY_RESOURCE_GROUP } \\ --name ${ IDENTITY_NAME } --query id --output tsv ) $ az aks pod-identity add --resource-group ${ AZURE_AKS_RESOURCE_GROUP } \\ --cluster-name ${ AZURE_AKS_CLUSTER_NAME } --namespace \"default\" \\ --name \"external-dns\" --identity-resource-id ${ IDENTITY_RESOURCE_ID } This will add something similar to the following resouces: apiVersion : aadpodidentity.k8s.io/v1 kind : AzureIdentity metadata : labels : addonmanager.kubernetes.io/mode : Reconcile kubernetes.azure.com/managedby : aks name : external-dns spec : clientID : $IDENTITY_CLIENT_ID resourceID : $IDENTITY_RESOURCE_ID type : 0 --- apiVersion : aadpodidentity.k8s.io/v1 kind : AzureIdentityBinding metadata : annotations : labels : addonmanager.kubernetes.io/mode : Reconcile kubernetes.azure.com/managedby : aks name : external-dns-binding spec : azureIdentity : external-dns selector : external-dns Update ExternalDNS labels \u00b6 When deploying ExternalDNS, you want to make sure that deployed pod(s) will have the label aadpodidbinding: external-dns to enable AAD Pod Identities. You can patch an existing deployment of ExternalDNS with this command: kubectl patch deployment external-dns --namespace \"default\" --patch \\ '{\"spec\": {\"template\": {\"metadata\": {\"labels\": {\"aadpodidbinding\": \"external-dns\"}}}}}' Managed identity using Workload Identity \u00b6 For this process, we will create a managed identity that will be explicitly used by the ExternalDNS container. This process is somewhat similar to Pod Identity except that this managed identity is associated with a kubernetes service account. Deploy OIDC issuer and Workload Identity services \u00b6 Update your cluster to install OIDC Issuer and Workload Identity : $ AZURE_AKS_RESOURCE_GROUP = \"my-aks-cluster-group\" # name of resource group where aks cluster was created $ AZURE_AKS_CLUSTER_NAME = \"my-aks-cluster\" # name of aks cluster previously created $ az aks update --resource-group ${ AZURE_AKS_RESOURCE_GROUP } --name ${ AZURE_AKS_CLUSTER_NAME } --enable-oidc-issuer --enable-workload-identity Create a managed identity \u00b6 Create a managed identity: $ IDENTITY_RESOURCE_GROUP = $AZURE_AKS_RESOURCE_GROUP # custom group or reuse AKS group $ IDENTITY_NAME = \"example-com-identity\" # create a managed identity $ az identity create --resource-group \" ${ IDENTITY_RESOURCE_GROUP } \" --name \" ${ IDENTITY_NAME } \" Assign a role to the managed identity \u00b6 Grant access to Azure DNS zone for the managed identity: $ AZURE_DNS_ZONE_RESOURCE_GROUP = \"MyDnsResourceGroup\" # name of resource group where dns zone is hosted $ AZURE_DNS_ZONE = \"example.com\" # DNS zone name like example.com or sub.example.com # fetch identity client id from managed identity created earlier $ IDENTITY_CLIENT_ID = $( az identity show --resource-group \" ${ IDENTITY_RESOURCE_GROUP } \" \\ --name \" ${ IDENTITY_NAME } \" --query \"clientId\" --output tsv ) # fetch DNS id used to grant access to the managed identity $ DNS_ID = $( az network dns zone show --name \" ${ AZURE_DNS_ZONE } \" \\ --resource-group \" ${ AZURE_DNS_ZONE_RESOURCE_GROUP } \" --query \"id\" --output tsv ) $ RESOURCE_GROUP_ID = $( az group show --name \" ${ AZURE_DNS_ZONE_RESOURCE_GROUP } \" --query \"id\" --output tsv ) $ az role assignment create --role \"DNS Zone Contributor\" \\ --assignee \" ${ IDENTITY_CLIENT_ID } \" --scope \" ${ DNS_ID } \" $ az role assignment create --role \"Reader\" \\ --assignee \" ${ IDENTITY_CLIENT_ID } \" --scope \" ${ RESOURCE_GROUP_ID } \" Create a federated identity credential \u00b6 A binding between the managed identity and the ExternalDNS service account needs to be setup by creating a federated identity resource: $ OIDC_ISSUER_URL = \" $( az aks show -n myAKSCluster -g myResourceGroup --query \"oidcIssuerProfile.issuerUrl\" -otsv ) \" $ az identity federated-credential create --name ${ IDENTITY_NAME } --identity-name ${ IDENTITY_NAME } --resource-group $AZURE_AKS_RESOURCE_GROUP } --issuer \" $OIDC_ISSUER_URL \" --subject \"system:serviceaccount:default:external-dns\" NOTE: make sure federated credential refers to correct namespace and service account ( system:serviceaccount:: ) helm \u00b6 When deploying external-dns with helm, here are the parameters you need to pass: fullnameOverride : external-dns serviceAccount : annotations : azure.workload.identity/client-id : podLabels : azure.workload.identity/use : \"true\" provider : azure secretConfiguration : enabled : true mountPath : \"/etc/kubernetes/\" data : azure.json : | { \"subscriptionId\": \"\", \"resourceGroup\": \"\", \"useWorkloadIdentityExtension\": true } NOTE: make sure the pod is restarted whenever you make a configuration change. kubectl (alternative) \u00b6 Create a configuration file for the managed identity \u00b6 Create the file azure.json with the values from previous steps: cat <<-EOF > /local/path/to/azure.json { \"subscriptionId\": \"$(az account show --query id -o tsv)\", \"resourceGroup\": \"$AZURE_DNS_ZONE_RESOURCE_GROUP\", \"useWorkloadIdentityExtension\": true } EOF Use the azure.json file to create a Kubernetes secret: $ kubectl create secret generic azure-config-file --namespace \"default\" --from-file /local/path/to/azure.json Update labels and annotations on ExternalDNS service account \u00b6 To instruct Workload Identity webhook to inject a projected token into the ExternalDNS pod, the pod needs to have a label azure.workload.identity/use: \"true\" (before Workload Identity 1.0.0, this label was supposed to be set on the service account instead). Also, the service account needs to have an annotation azure.workload.identity/client-id: : To patch the existing serviceaccount and deployment, use the following command: $ kubectl patch serviceaccount external-dns --namespace \"default\" --patch \\ \"{\\\"metadata\\\": {\\\"annotations\\\": {\\\"azure.workload.identity/client-id\\\": \\\" ${ IDENTITY_CLIENT_ID } \\\"}}}\" $ kubectl patch deployment external-dns --namespace \"default\" --patch \\ '{\"spec\": {\"template\": {\"metadata\": {\"labels\": {\\\"azure.workload.identity/use\\\": \\\"true\\\"}}}}}' NOTE: it\u2019s also possible to specify (or override) ClientID through UserAssignedIdentityID field in azure.json . NOTE: make sure the pod is restarted whenever you make a configuration change. Ingress used with ExternalDNS \u00b6 This deployment assumes that you will be using nginx-ingress. When using nginx-ingress do not deploy it as a Daemon Set. This causes nginx-ingress to write the Cluster IP of the backend pods in the ingress status.loadbalancer.ip property which then has external-dns write the Cluster IP(s) in DNS vs. the nginx-ingress service external IP. Ensure that your nginx-ingress deployment has the following arg: added to it: - --publish-service=namespace/nginx-ingress-controller-svcname For more details see here: nginx-ingress external-dns Deploy ExternalDNS \u00b6 Connect your kubectl client to the cluster you want to test ExternalDNS with. Then apply one of the following manifests file to deploy ExternalDNS. The deployment assumes that ExternalDNS will be installed into the default namespace. If this namespace is different, the ClusterRoleBinding will need to be updated to reflect the desired alternative namespace, such as external-dns , kube-addons , etc. Manifest (for clusters without RBAC enabled) \u00b6 apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 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=MyDnsResourceGroup # (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 Manifest (for clusters with RBAC enabled, cluster access) \u00b6 apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" , \"nodes\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 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=MyDnsResourceGroup # (optional) use the DNS zones from the tutorial's resource group - --txt-prefix=externaldns- volumeMounts : - name : azure-config-file mountPath : /etc/kubernetes readOnly : true volumes : - name : azure-config-file secret : secretName : azure-config-file Manifest (for clusters with RBAC enabled, namespace access) \u00b6 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 requires cluster access, so when using this manifest, services with type NodePort will be skipped! apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : Role metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : RoleBinding metadata : name : external-dns roleRef : apiGroup : rbac.authorization.k8s.io kind : Role name : external-dns subjects : - kind : ServiceAccount name : external-dns --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 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=MyDnsResourceGroup # (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: $ kubectl create --namespace \"default\" --filename externaldns.yaml Ingress Option: Expose an nginx service with an ingress \u00b6 Create a file called nginx.yaml with the following contents: apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 --- apiVersion : v1 kind : Service metadata : name : nginx-svc spec : ports : - port : 80 protocol : TCP targetPort : 80 selector : app : nginx type : ClusterIP --- apiVersion : networking.k8s.io/v1 kind : Ingress metadata : name : nginx spec : ingressClassName : nginx rules : - host : server.example.com http : paths : - path : / pathType : Prefix backend : service : name : nginx-svc port : number : 80 When using ExternalDNS with ingress objects it will automatically create DNS records based on host names specified in ingress objects that match the domain-filter argument in the external-dns deployment manifest. When those host names are removed or renamed the corresponding DNS records are also altered. Create the deployment, service and ingress object: $ kubectl create --namespace \"default\" --filename nginx.yaml Since your external IP would have already been assigned to the nginx-ingress service, the DNS records pointing to the IP of the nginx-ingress service should be created within a minute. Azure Load Balancer option: Expose an nginx service with a load balancer \u00b6 Create a file called nginx.yaml with the following contents: --- apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 --- apiVersion : v1 kind : Service annotations : external-dns.alpha.kubernetes.io/hostname : server.example.com metadata : name : nginx-svc spec : ports : - port : 80 protocol : TCP targetPort : 80 selector : app : nginx type : LoadBalancer The annotation external-dns.alpha.kubernetes.io/hostname is used to specify the DNS name that should be created for the service. The annotation value is a comma separated list of host names. Verifying Azure DNS records \u00b6 Run the following command to view the A records for your Azure DNS zone: $ az network dns record-set a list --resource-group \"MyDnsResourceGroup\" --zone-name example.com Substitute the zone for the one created above if a different domain was used. This should show the external IP address of the service as the A record for your domain (\u2018@\u2019 indicates the record is for the zone itself). Delete Azure Resource Group \u00b6 Now that we have verified that ExternalDNS will automatically manage Azure DNS records, we can delete the tutorial\u2019s resource group: $ az group delete --name \"MyDnsResourceGroup\" More tutorials \u00b6 A video explanantion is available here: https://www.youtube.com/watch?v=VSn6DPKIhM8&list=PLpbcUe4chE79sB7Jg7B4z3HytqUUEwcNE","title":"Setting up ExternalDNS for Services on Azure"},{"location":"tutorials/azure/#setting-up-externaldns-for-services-on-azure","text":"This tutorial describes how to setup ExternalDNS for Azure DNS with Azure Kubernetes Service . Make sure to use >=0.11.0 version of ExternalDNS for this tutorial. This tutorial uses Azure CLI 2.0 for all Azure commands and assumes that the Kubernetes cluster was created via Azure Container Services and kubectl commands are being run on an orchestration node.","title":"Setting up ExternalDNS for Services on Azure"},{"location":"tutorials/azure/#creating-an-azure-dns-zone","text":"The Azure provider for ExternalDNS will find suitable zones for domains it manages; it will not automatically create zones. For this tutorial, we will create a Azure resource group named MyDnsResourceGroup that can easily be deleted later: $ az group create --name \"MyDnsResourceGroup\" --location \"eastus\" Substitute a more suitable location for the resource group if desired. Next, create a Azure DNS zone for example.com : $ az network dns zone create --resource-group \"MyDnsResourceGroup\" --name \"example.com\" Substitute a domain you own for example.com if desired. If using your own domain that was registered with a third-party domain registrar, you should point your domain\u2019s name servers to the values in the nameServers field from the JSON data returned by the az network dns zone create command. Please consult your registrar\u2019s documentation on how to do that.","title":"Creating an Azure DNS zone"},{"location":"tutorials/azure/#configuration-file","text":"The azure provider will reference a configuration file called azure.json . The preferred way to inject the configuration file is by using a Kubernetes secret. The secret should contain an object named azure.json with content similar to this: { \"tenantId\" : \"01234abc-de56-ff78-abc1-234567890def\" , \"subscriptionId\" : \"01234abc-de56-ff78-abc1-234567890def\" , \"resourceGroup\" : \"MyDnsResourceGroup\" , \"aadClientId\" : \"01234abc-de56-ff78-abc1-234567890def\" , \"aadClientSecret\" : \"uKiuXeiwui4jo9quae9o\" } The following fields are used: tenantId ( required ) - run az account show --query \"tenantId\" or by selecting Azure Active Directory in the Azure Portal and checking the Directory ID under Properties. subscriptionId ( required ) - run az account show --query \"id\" or by selecting Subscriptions in the Azure Portal. resourceGroup ( required ) is the Resource Group created in a previous step that contains the Azure DNS Zone. aadClientID and aadClientSecret are associated with the Service Principal. This is only used with Service Principal method documented in the next section. useManagedIdentityExtension - this is set to true if you use either AKS Kubelet Identity or AAD Pod Identities methods documented in the next section. userAssignedIdentityID - this contains the client id from the Managed identitty when using the AAD Pod Identities method documented in the next setion. useWorkloadIdentityExtension - this is set to true if you use Workload Identity method documented in the next section. 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.","title":"Configuration file"},{"location":"tutorials/azure/#permissions-to-modify-dns-zone","text":"ExternalDNS needs permissions to make changes to the Azure DNS zone. There are three ways configure the access needed: Service Principal Managed Identity Using AKS Kubelet Identity Managed Identity Using AAD Pod Identities Managed Identity Using Workload Identity","title":"Permissions to modify DNS zone"},{"location":"tutorials/azure/#service-principal","text":"These permissions are defined in a Service Principal that should be made available to ExternalDNS as a configuration file azure.json .","title":"Service Principal"},{"location":"tutorials/azure/#creating-a-service-principal","text":"A Service Principal with a minimum access level of DNS Zone Contributor or Contributor to the DNS zone(s) and Reader to the resource group containing the Azure DNS zone(s) is necessary for ExternalDNS to be able to edit DNS records. However, other more permissive access levels will work too (e.g. Contributor to the resource group or the whole subscription). This is an Azure CLI example on how to query the Azure API for the information required for the Resource Group and DNS zone you would have already created in previous steps (requires azure-cli and jq ) $ EXTERNALDNS_NEW_SP_NAME = \"ExternalDnsServicePrincipal\" # name of the service principal $ AZURE_DNS_ZONE_RESOURCE_GROUP = \"MyDnsResourceGroup\" # name of resource group where dns zone is hosted $ AZURE_DNS_ZONE = \"example.com\" # DNS zone name like example.com or sub.example.com # Create the service principal $ DNS_SP = $( az ad sp create-for-rbac --name $EXTERNALDNS_NEW_SP_NAME ) $ EXTERNALDNS_SP_APP_ID = $( echo $DNS_SP | jq -r '.appId' ) $ EXTERNALDNS_SP_PASSWORD = $( echo $DNS_SP | jq -r '.password' )","title":"Creating a service principal"},{"location":"tutorials/azure/#assign-the-rights-for-the-service-principal","text":"Grant access to Azure DNS zone for the service principal. # fetch DNS id used to grant access to the service principal DNS_ID = $( az network dns zone show --name $AZURE_DNS_ZONE \\ --resource-group $AZURE_DNS_ZONE_RESOURCE_GROUP --query \"id\" --output tsv ) # 1. as a reader to the resource group $ az role assignment create --role \"Reader\" --assignee $EXTERNALDNS_SP_APP_ID --scope $DNS_ID # 2. as a contributor to DNS Zone itself $ az role assignment create --role \"Contributor\" --assignee $EXTERNALDNS_SP_APP_ID --scope $DNS_ID","title":"Assign the rights for the service principal"},{"location":"tutorials/azure/#creating-a-configuration-file-for-the-service-principal","text":"Create the file azure.json with values gather from previous steps. cat <<-EOF > /local/path/to/azure.json { \"tenantId\": \"$(az account show --query tenantId -o tsv)\", \"subscriptionId\": \"$(az account show --query id -o tsv)\", \"resourceGroup\": \"$AZURE_DNS_ZONE_RESOURCE_GROUP\", \"aadClientId\": \"$EXTERNALDNS_SP_APP_ID\", \"aadClientSecret\": \"$EXTERNALDNS_SP_PASSWORD\" } EOF Use this file to create a Kubernetes secret: $ kubectl create secret generic azure-config-file --namespace \"default\" --from-file /local/path/to/azure.json","title":"Creating a configuration file for the service principal"},{"location":"tutorials/azure/#managed-identity-using-aks-kubelet-identity","text":"The managed identity that is assigned to the underlying node pool in the AKS cluster can be given permissions to access Azure DNS. Managed identities are essentially a service principal whose lifecycle is managed, such as deleting the AKS cluster will also delete the service principals associated with the AKS cluster. The managed identity assigned Kuberetes node pool, or specifically the VMSS , is called the Kubelet identity. The managed identites were previously called MSI (Managed Service Identity) and are enabled by default when creating an AKS cluster. Note that permissions granted to this identity will be accessible to all containers running inside the Kubernetes cluster, not just the ExternalDNS container(s). For the managed identity, the contents of azure.json should be similar to this: { \"tenantId\" : \"01234abc-de56-ff78-abc1-234567890def\" , \"subscriptionId\" : \"01234abc-de56-ff78-abc1-234567890def\" , \"resourceGroup\" : \"MyDnsResourceGroup\" , \"useManagedIdentityExtension\" : true }","title":"Managed identity using AKS Kubelet identity"},{"location":"tutorials/azure/#fetching-the-kubelet-identity","text":"For this process, you will need to get the kublet identity: $ PRINCIPAL_ID = $( az aks show --resource-group $CLUSTER_GROUP --name $CLUSTERNAME \\ --query \"identityProfile.kubeletidentity.objectId\" --output tsv )","title":"Fetching the Kubelet identity"},{"location":"tutorials/azure/#assign-rights-for-the-kubelet-identity","text":"Grant access to Azure DNS zone for the kublet identity. $ AZURE_DNS_ZONE = \"example.com\" # DNS zone name like example.com or sub.example.com $ AZURE_DNS_ZONE_RESOURCE_GROUP = \"MyDnsResourceGroup\" # resource group where DNS zone is hosted # fetch DNS id used to grant access to the kublet identity $ DNS_ID = $( az network dns zone show --name $AZURE_DNS_ZONE \\ --resource-group $AZURE_DNS_ZONE_RESOURCE_GROUP --query \"id\" --output tsv ) $ az role assignment create --role \"DNS Zone Contributor\" --assignee $PRINCIPAL_ID --scope $DNS_ID","title":"Assign rights for the Kubelet identity"},{"location":"tutorials/azure/#creating-a-configuration-file-for-the-kubelet-identity","text":"Create the file azure.json with values gather from previous steps. cat <<-EOF > /local/path/to/azure.json { \"tenantId\": \"$(az account show --query tenantId -o tsv)\", \"subscriptionId\": \"$(az account show --query id -o tsv)\", \"resourceGroup\": \"$AZURE_DNS_ZONE_RESOURCE_GROUP\", \"useManagedIdentityExtension\": true } EOF Use the azure.json file to create a Kubernetes secret: $ kubectl create secret generic azure-config-file --namespace \"default\" --from-file /local/path/to/azure.json","title":"Creating a configuration file for the kubelet identity"},{"location":"tutorials/azure/#managed-identity-using-aad-pod-identities","text":"For this process, we will create a managed identity that will be explicitly used by the ExternalDNS container. This process is similar to Kubelet identity except that this managed identity is not associated with the Kubernetes node pool, but rather associated with explicit ExternalDNS containers.","title":"Managed identity using AAD Pod Identities"},{"location":"tutorials/azure/#enable-the-aad-pod-identities-feature","text":"For this solution, AAD Pod Identities preview feature can be enabled. The commands below should do the trick to enable this feature: $ az feature register --name EnablePodIdentityPreview --namespace Microsoft.ContainerService $ az feature register --name AutoUpgradePreview --namespace Microsoft.ContainerService $ az extension add --name aks-preview $ az extension update --name aks-preview $ az provider register --namespace Microsoft.ContainerService","title":"Enable the AAD Pod Identities feature"},{"location":"tutorials/azure/#deploy-the-aad-pod-identities-service","text":"Once enabled, you can update your cluster and install needed services for the AAD Pod Identities feature. $ AZURE_AKS_RESOURCE_GROUP = \"my-aks-cluster-group\" # name of resource group where aks cluster was created $ AZURE_AKS_CLUSTER_NAME = \"my-aks-cluster\" # name of aks cluster previously created $ az aks update --resource-group ${ AZURE_AKS_RESOURCE_GROUP } --name ${ AZURE_AKS_CLUSTER_NAME } --enable-pod-identity Note that, if you use the default network plugin kubenet , then you need to add the command line option --enable-pod-identity-with-kubenet to the above command.","title":"Deploy the AAD Pod Identities service"},{"location":"tutorials/azure/#creating-the-managed-identity","text":"After this process is finished, create a managed identity. $ IDENTITY_RESOURCE_GROUP = $AZURE_AKS_RESOURCE_GROUP # custom group or reuse AKS group $ IDENTITY_NAME = \"example-com-identity\" # create a managed identity $ az identity create --resource-group \" ${ IDENTITY_RESOURCE_GROUP } \" --name \" ${ IDENTITY_NAME } \"","title":"Creating the managed identity"},{"location":"tutorials/azure/#assign-rights-for-the-managed-identity","text":"Grant access to Azure DNS zone for the managed identity. $ AZURE_DNS_ZONE_RESOURCE_GROUP = \"MyDnsResourceGroup\" # name of resource group where dns zone is hosted $ AZURE_DNS_ZONE = \"example.com\" # DNS zone name like example.com or sub.example.com # fetch identity client id from managed identity created earlier $ IDENTITY_CLIENT_ID = $( az identity show --resource-group \" ${ IDENTITY_RESOURCE_GROUP } \" \\ --name \" ${ IDENTITY_NAME } \" --query \"clientId\" --output tsv ) # fetch DNS id used to grant access to the managed identity $ DNS_ID = $( az network dns zone show --name \" ${ AZURE_DNS_ZONE } \" \\ --resource-group \" ${ AZURE_DNS_ZONE_RESOURCE_GROUP } \" --query \"id\" --output tsv ) $ az role assignment create --role \"DNS Zone Contributor\" \\ --assignee \" ${ IDENTITY_CLIENT_ID } \" --scope \" ${ DNS_ID } \"","title":"Assign rights for the managed identity"},{"location":"tutorials/azure/#creating-a-configuration-file-for-the-managed-identity","text":"Create the file azure.json with the values from previous steps: cat <<-EOF > /local/path/to/azure.json { \"tenantId\": \"$(az account show --query tenantId -o tsv)\", \"subscriptionId\": \"$(az account show --query id -o tsv)\", \"resourceGroup\": \"$AZURE_DNS_ZONE_RESOURCE_GROUP\", \"useManagedIdentityExtension\": true, \"userAssignedIdentityID\": \"$IDENTITY_CLIENT_ID\" } EOF Use the azure.json file to create a Kubernetes secret: $ kubectl create secret generic azure-config-file --namespace \"default\" --from-file /local/path/to/azure.json","title":"Creating a configuration file for the managed identity"},{"location":"tutorials/azure/#creating-an-azure-identity-binding","text":"A binding between the managed identity and the ExternalDNS pods needs to be setup by creating AzureIdentity and AzureIdentityBinding resources. This will allow appropriately labeled ExternalDNS pods to authenticate using the managed identity. When AAD Pod Identity feature is enabled from previous steps above, the az aks pod-identity add can be used to create these resources: $ IDENTITY_RESOURCE_ID = $( az identity show --resource-group ${ IDENTITY_RESOURCE_GROUP } \\ --name ${ IDENTITY_NAME } --query id --output tsv ) $ az aks pod-identity add --resource-group ${ AZURE_AKS_RESOURCE_GROUP } \\ --cluster-name ${ AZURE_AKS_CLUSTER_NAME } --namespace \"default\" \\ --name \"external-dns\" --identity-resource-id ${ IDENTITY_RESOURCE_ID } This will add something similar to the following resouces: apiVersion : aadpodidentity.k8s.io/v1 kind : AzureIdentity metadata : labels : addonmanager.kubernetes.io/mode : Reconcile kubernetes.azure.com/managedby : aks name : external-dns spec : clientID : $IDENTITY_CLIENT_ID resourceID : $IDENTITY_RESOURCE_ID type : 0 --- apiVersion : aadpodidentity.k8s.io/v1 kind : AzureIdentityBinding metadata : annotations : labels : addonmanager.kubernetes.io/mode : Reconcile kubernetes.azure.com/managedby : aks name : external-dns-binding spec : azureIdentity : external-dns selector : external-dns","title":"Creating an Azure identity binding"},{"location":"tutorials/azure/#update-externaldns-labels","text":"When deploying ExternalDNS, you want to make sure that deployed pod(s) will have the label aadpodidbinding: external-dns to enable AAD Pod Identities. You can patch an existing deployment of ExternalDNS with this command: kubectl patch deployment external-dns --namespace \"default\" --patch \\ '{\"spec\": {\"template\": {\"metadata\": {\"labels\": {\"aadpodidbinding\": \"external-dns\"}}}}}'","title":"Update ExternalDNS labels"},{"location":"tutorials/azure/#managed-identity-using-workload-identity","text":"For this process, we will create a managed identity that will be explicitly used by the ExternalDNS container. This process is somewhat similar to Pod Identity except that this managed identity is associated with a kubernetes service account.","title":"Managed identity using Workload Identity"},{"location":"tutorials/azure/#deploy-oidc-issuer-and-workload-identity-services","text":"Update your cluster to install OIDC Issuer and Workload Identity : $ AZURE_AKS_RESOURCE_GROUP = \"my-aks-cluster-group\" # name of resource group where aks cluster was created $ AZURE_AKS_CLUSTER_NAME = \"my-aks-cluster\" # name of aks cluster previously created $ az aks update --resource-group ${ AZURE_AKS_RESOURCE_GROUP } --name ${ AZURE_AKS_CLUSTER_NAME } --enable-oidc-issuer --enable-workload-identity","title":"Deploy OIDC issuer and Workload Identity services"},{"location":"tutorials/azure/#create-a-managed-identity","text":"Create a managed identity: $ IDENTITY_RESOURCE_GROUP = $AZURE_AKS_RESOURCE_GROUP # custom group or reuse AKS group $ IDENTITY_NAME = \"example-com-identity\" # create a managed identity $ az identity create --resource-group \" ${ IDENTITY_RESOURCE_GROUP } \" --name \" ${ IDENTITY_NAME } \"","title":"Create a managed identity"},{"location":"tutorials/azure/#assign-a-role-to-the-managed-identity","text":"Grant access to Azure DNS zone for the managed identity: $ AZURE_DNS_ZONE_RESOURCE_GROUP = \"MyDnsResourceGroup\" # name of resource group where dns zone is hosted $ AZURE_DNS_ZONE = \"example.com\" # DNS zone name like example.com or sub.example.com # fetch identity client id from managed identity created earlier $ IDENTITY_CLIENT_ID = $( az identity show --resource-group \" ${ IDENTITY_RESOURCE_GROUP } \" \\ --name \" ${ IDENTITY_NAME } \" --query \"clientId\" --output tsv ) # fetch DNS id used to grant access to the managed identity $ DNS_ID = $( az network dns zone show --name \" ${ AZURE_DNS_ZONE } \" \\ --resource-group \" ${ AZURE_DNS_ZONE_RESOURCE_GROUP } \" --query \"id\" --output tsv ) $ RESOURCE_GROUP_ID = $( az group show --name \" ${ AZURE_DNS_ZONE_RESOURCE_GROUP } \" --query \"id\" --output tsv ) $ az role assignment create --role \"DNS Zone Contributor\" \\ --assignee \" ${ IDENTITY_CLIENT_ID } \" --scope \" ${ DNS_ID } \" $ az role assignment create --role \"Reader\" \\ --assignee \" ${ IDENTITY_CLIENT_ID } \" --scope \" ${ RESOURCE_GROUP_ID } \"","title":"Assign a role to the managed identity"},{"location":"tutorials/azure/#create-a-federated-identity-credential","text":"A binding between the managed identity and the ExternalDNS service account needs to be setup by creating a federated identity resource: $ OIDC_ISSUER_URL = \" $( az aks show -n myAKSCluster -g myResourceGroup --query \"oidcIssuerProfile.issuerUrl\" -otsv ) \" $ az identity federated-credential create --name ${ IDENTITY_NAME } --identity-name ${ IDENTITY_NAME } --resource-group $AZURE_AKS_RESOURCE_GROUP } --issuer \" $OIDC_ISSUER_URL \" --subject \"system:serviceaccount:default:external-dns\" NOTE: make sure federated credential refers to correct namespace and service account ( system:serviceaccount:: )","title":"Create a federated identity credential"},{"location":"tutorials/azure/#helm","text":"When deploying external-dns with helm, here are the parameters you need to pass: fullnameOverride : external-dns serviceAccount : annotations : azure.workload.identity/client-id : podLabels : azure.workload.identity/use : \"true\" provider : azure secretConfiguration : enabled : true mountPath : \"/etc/kubernetes/\" data : azure.json : | { \"subscriptionId\": \"\", \"resourceGroup\": \"\", \"useWorkloadIdentityExtension\": true } NOTE: make sure the pod is restarted whenever you make a configuration change.","title":"helm"},{"location":"tutorials/azure/#kubectl-alternative","text":"","title":"kubectl (alternative)"},{"location":"tutorials/azure/#create-a-configuration-file-for-the-managed-identity","text":"Create the file azure.json with the values from previous steps: cat <<-EOF > /local/path/to/azure.json { \"subscriptionId\": \"$(az account show --query id -o tsv)\", \"resourceGroup\": \"$AZURE_DNS_ZONE_RESOURCE_GROUP\", \"useWorkloadIdentityExtension\": true } EOF Use the azure.json file to create a Kubernetes secret: $ kubectl create secret generic azure-config-file --namespace \"default\" --from-file /local/path/to/azure.json","title":"Create a configuration file for the managed identity"},{"location":"tutorials/azure/#update-labels-and-annotations-on-externaldns-service-account","text":"To instruct Workload Identity webhook to inject a projected token into the ExternalDNS pod, the pod needs to have a label azure.workload.identity/use: \"true\" (before Workload Identity 1.0.0, this label was supposed to be set on the service account instead). Also, the service account needs to have an annotation azure.workload.identity/client-id: : To patch the existing serviceaccount and deployment, use the following command: $ kubectl patch serviceaccount external-dns --namespace \"default\" --patch \\ \"{\\\"metadata\\\": {\\\"annotations\\\": {\\\"azure.workload.identity/client-id\\\": \\\" ${ IDENTITY_CLIENT_ID } \\\"}}}\" $ kubectl patch deployment external-dns --namespace \"default\" --patch \\ '{\"spec\": {\"template\": {\"metadata\": {\"labels\": {\\\"azure.workload.identity/use\\\": \\\"true\\\"}}}}}' NOTE: it\u2019s also possible to specify (or override) ClientID through UserAssignedIdentityID field in azure.json . NOTE: make sure the pod is restarted whenever you make a configuration change.","title":"Update labels and annotations on ExternalDNS service account"},{"location":"tutorials/azure/#ingress-used-with-externaldns","text":"This deployment assumes that you will be using nginx-ingress. When using nginx-ingress do not deploy it as a Daemon Set. This causes nginx-ingress to write the Cluster IP of the backend pods in the ingress status.loadbalancer.ip property which then has external-dns write the Cluster IP(s) in DNS vs. the nginx-ingress service external IP. Ensure that your nginx-ingress deployment has the following arg: added to it: - --publish-service=namespace/nginx-ingress-controller-svcname For more details see here: nginx-ingress external-dns","title":"Ingress used with ExternalDNS"},{"location":"tutorials/azure/#deploy-externaldns","text":"Connect your kubectl client to the cluster you want to test ExternalDNS with. Then apply one of the following manifests file to deploy ExternalDNS. The deployment assumes that ExternalDNS will be installed into the default namespace. If this namespace is different, the ClusterRoleBinding will need to be updated to reflect the desired alternative namespace, such as external-dns , kube-addons , etc.","title":"Deploy ExternalDNS"},{"location":"tutorials/azure/#manifest-for-clusters-without-rbac-enabled","text":"apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 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=MyDnsResourceGroup # (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","title":"Manifest (for clusters without RBAC enabled)"},{"location":"tutorials/azure/#manifest-for-clusters-with-rbac-enabled-cluster-access","text":"apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" , \"nodes\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 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=MyDnsResourceGroup # (optional) use the DNS zones from the tutorial's resource group - --txt-prefix=externaldns- volumeMounts : - name : azure-config-file mountPath : /etc/kubernetes readOnly : true volumes : - name : azure-config-file secret : secretName : azure-config-file","title":"Manifest (for clusters with RBAC enabled, cluster access)"},{"location":"tutorials/azure/#manifest-for-clusters-with-rbac-enabled-namespace-access","text":"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 requires cluster access, so when using this manifest, services with type NodePort will be skipped! apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : Role metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : RoleBinding metadata : name : external-dns roleRef : apiGroup : rbac.authorization.k8s.io kind : Role name : external-dns subjects : - kind : ServiceAccount name : external-dns --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 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=MyDnsResourceGroup # (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: $ kubectl create --namespace \"default\" --filename externaldns.yaml","title":"Manifest (for clusters with RBAC enabled, namespace access)"},{"location":"tutorials/azure/#ingress-option-expose-an-nginx-service-with-an-ingress","text":"Create a file called nginx.yaml with the following contents: apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 --- apiVersion : v1 kind : Service metadata : name : nginx-svc spec : ports : - port : 80 protocol : TCP targetPort : 80 selector : app : nginx type : ClusterIP --- apiVersion : networking.k8s.io/v1 kind : Ingress metadata : name : nginx spec : ingressClassName : nginx rules : - host : server.example.com http : paths : - path : / pathType : Prefix backend : service : name : nginx-svc port : number : 80 When using ExternalDNS with ingress objects it will automatically create DNS records based on host names specified in ingress objects that match the domain-filter argument in the external-dns deployment manifest. When those host names are removed or renamed the corresponding DNS records are also altered. Create the deployment, service and ingress object: $ kubectl create --namespace \"default\" --filename nginx.yaml Since your external IP would have already been assigned to the nginx-ingress service, the DNS records pointing to the IP of the nginx-ingress service should be created within a minute.","title":"Ingress Option: Expose an nginx service with an ingress"},{"location":"tutorials/azure/#azure-load-balancer-option-expose-an-nginx-service-with-a-load-balancer","text":"Create a file called nginx.yaml with the following contents: --- apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 --- apiVersion : v1 kind : Service annotations : external-dns.alpha.kubernetes.io/hostname : server.example.com metadata : name : nginx-svc spec : ports : - port : 80 protocol : TCP targetPort : 80 selector : app : nginx type : LoadBalancer The annotation external-dns.alpha.kubernetes.io/hostname is used to specify the DNS name that should be created for the service. The annotation value is a comma separated list of host names.","title":"Azure Load Balancer option: Expose an nginx service with a load balancer"},{"location":"tutorials/azure/#verifying-azure-dns-records","text":"Run the following command to view the A records for your Azure DNS zone: $ az network dns record-set a list --resource-group \"MyDnsResourceGroup\" --zone-name example.com Substitute the zone for the one created above if a different domain was used. This should show the external IP address of the service as the A record for your domain (\u2018@\u2019 indicates the record is for the zone itself).","title":"Verifying Azure DNS records"},{"location":"tutorials/azure/#delete-azure-resource-group","text":"Now that we have verified that ExternalDNS will automatically manage Azure DNS records, we can delete the tutorial\u2019s resource group: $ az group delete --name \"MyDnsResourceGroup\"","title":"Delete Azure Resource Group"},{"location":"tutorials/azure/#more-tutorials","text":"A video explanantion is available here: https://www.youtube.com/watch?v=VSn6DPKIhM8&list=PLpbcUe4chE79sB7Jg7B4z3HytqUUEwcNE","title":"More tutorials"},{"location":"tutorials/bluecat/","text":"Setting up external-dns for BlueCat \u00b6 The first external-dns release with with BlueCat provider support is v0.8.0. Prerequisites \u00b6 Install the BlueCat Gateway product and deploy the community gateway workflows . Configuration Options \u00b6 There are two ways to pass configuration options to the Bluecat Provider JSON configuration file and command line flags. Currently if a valid configuration file is used all BlueCat provider configurations will be taken from the configuration file. If a configuraiton file is not provided or cannot be read then all BlueCat provider configurations will be taken from the command line flags. In the future an enhancement will be made to merge configuration options from the configuration file and command line flags if both are provided. BlueCat provider supports getting the proxy URL from the environment variables. The format is the one specified by golang\u2019s http.ProxyFromEnvironment . Using CLI Flags \u00b6 When using CLI flags to configure the Bluecat Provider the BlueCat Gateway credentials are passed in using environment variables BLUECAT_USERNAME and BLUECAT_PASSWORD . Deploy \u00b6 Setup up namespace, deployment, and service account: kubectl create namespace bluecat-example kubectl create secret generic bluecat-credentials --from-literal=username=bluecatuser --from-literal=password=bluecatpassword -n bluecat-example cat << EOF > ~/bluecat.yml --- apiVersion: v1 kind: ServiceAccount metadata: name: external-dns --- apiVersion: apps/v1 kind: Deployment metadata: name: external-dns spec: selector: matchLabels: app: external-dns strategy: type: Recreate template: metadata: labels: app: external-dns spec: serviceAccountName: external-dns containers: - name: external-dns image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --log-level=debug - --source=service - --provider=bluecat - --txt-owner-id=bluecat-example - --bluecat-dns-configuration=Example - --bluecat-dns-view=Internal - --bluecat-gateway-host=https://bluecatgw.example.com - --bluecat-root-zone=example.com env: - name: BLUECAT_USERNAME valueFrom: secretKeyRef: name: bluecat-credentials key: username - name: BLUECAT_PASSWORD valueFrom: secretKeyRef: name: bluecat-credentials key: password EOF kubectl apply -f ~/bluecat.yml -n bluecat-example Using JSON Configuration File \u00b6 The options for configuring the Bluecat Provider are available through the JSON file provided to External-DNS via the flag --bluecat-config-file . Key Required gatewayHost Yes gatewayUsername No gatewayPassword No dnsConfiguration Yes dnsView Yes rootZone Yes dnsServerName No dnsDeployType No skipTLSVerify No (default false) Deploy \u00b6 Setup configuration file as k8s Secret . cat << EOF > ~/bluecat.json { \"gatewayHost\": \"https://bluecatgw.example.com\", \"gatewayUsername\": \"user\", \"gatewayPassword\": \"pass\", \"dnsConfiguration\": \"Example\", \"dnsView\": \"Internal\", \"rootZone\": \"example.com\", \"skipTLSVerify\": false } EOF kubectl create secret generic bluecatconfig --from-file ~/bluecat.json -n bluecat-example Setup up namespace, deployment, and service account: kubectl create namespace bluecat-example cat << EOF > ~/bluecat.yml --- apiVersion: v1 kind: ServiceAccount metadata: name: external-dns --- apiVersion: apps/v1 kind: Deployment metadata: name: external-dns spec: selector: matchLabels: app: external-dns strategy: type: Recreate template: metadata: labels: app: external-dns spec: serviceAccountName: external-dns volumes: - name: bluecatconfig secret: secretName: bluecatconfig containers: - name: external-dns image: registry.k8s.io/external-dns/external-dns:v0.13.5 volumeMounts: - name: bluecatconfig mountPath: \"/etc/external-dns/\" readOnly: true args: - --log-level=debug - --source=service - --provider=bluecat - --txt-owner-id=bluecat-example - --bluecat-config-file=/etc/external-dns/bluecat.json EOF kubectl apply -f ~/bluecat.yml -n bluecat-example","title":"Setting up external-dns for BlueCat"},{"location":"tutorials/bluecat/#setting-up-external-dns-for-bluecat","text":"The first external-dns release with with BlueCat provider support is v0.8.0.","title":"Setting up external-dns for BlueCat"},{"location":"tutorials/bluecat/#prerequisites","text":"Install the BlueCat Gateway product and deploy the community gateway workflows .","title":"Prerequisites"},{"location":"tutorials/bluecat/#configuration-options","text":"There are two ways to pass configuration options to the Bluecat Provider JSON configuration file and command line flags. Currently if a valid configuration file is used all BlueCat provider configurations will be taken from the configuration file. If a configuraiton file is not provided or cannot be read then all BlueCat provider configurations will be taken from the command line flags. In the future an enhancement will be made to merge configuration options from the configuration file and command line flags if both are provided. BlueCat provider supports getting the proxy URL from the environment variables. The format is the one specified by golang\u2019s http.ProxyFromEnvironment .","title":"Configuration Options"},{"location":"tutorials/bluecat/#using-cli-flags","text":"When using CLI flags to configure the Bluecat Provider the BlueCat Gateway credentials are passed in using environment variables BLUECAT_USERNAME and BLUECAT_PASSWORD .","title":"Using CLI Flags"},{"location":"tutorials/bluecat/#deploy","text":"Setup up namespace, deployment, and service account: kubectl create namespace bluecat-example kubectl create secret generic bluecat-credentials --from-literal=username=bluecatuser --from-literal=password=bluecatpassword -n bluecat-example cat << EOF > ~/bluecat.yml --- apiVersion: v1 kind: ServiceAccount metadata: name: external-dns --- apiVersion: apps/v1 kind: Deployment metadata: name: external-dns spec: selector: matchLabels: app: external-dns strategy: type: Recreate template: metadata: labels: app: external-dns spec: serviceAccountName: external-dns containers: - name: external-dns image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --log-level=debug - --source=service - --provider=bluecat - --txt-owner-id=bluecat-example - --bluecat-dns-configuration=Example - --bluecat-dns-view=Internal - --bluecat-gateway-host=https://bluecatgw.example.com - --bluecat-root-zone=example.com env: - name: BLUECAT_USERNAME valueFrom: secretKeyRef: name: bluecat-credentials key: username - name: BLUECAT_PASSWORD valueFrom: secretKeyRef: name: bluecat-credentials key: password EOF kubectl apply -f ~/bluecat.yml -n bluecat-example","title":"Deploy"},{"location":"tutorials/bluecat/#using-json-configuration-file","text":"The options for configuring the Bluecat Provider are available through the JSON file provided to External-DNS via the flag --bluecat-config-file . Key Required gatewayHost Yes gatewayUsername No gatewayPassword No dnsConfiguration Yes dnsView Yes rootZone Yes dnsServerName No dnsDeployType No skipTLSVerify No (default false)","title":"Using JSON Configuration File"},{"location":"tutorials/bluecat/#deploy_1","text":"Setup configuration file as k8s Secret . cat << EOF > ~/bluecat.json { \"gatewayHost\": \"https://bluecatgw.example.com\", \"gatewayUsername\": \"user\", \"gatewayPassword\": \"pass\", \"dnsConfiguration\": \"Example\", \"dnsView\": \"Internal\", \"rootZone\": \"example.com\", \"skipTLSVerify\": false } EOF kubectl create secret generic bluecatconfig --from-file ~/bluecat.json -n bluecat-example Setup up namespace, deployment, and service account: kubectl create namespace bluecat-example cat << EOF > ~/bluecat.yml --- apiVersion: v1 kind: ServiceAccount metadata: name: external-dns --- apiVersion: apps/v1 kind: Deployment metadata: name: external-dns spec: selector: matchLabels: app: external-dns strategy: type: Recreate template: metadata: labels: app: external-dns spec: serviceAccountName: external-dns volumes: - name: bluecatconfig secret: secretName: bluecatconfig containers: - name: external-dns image: registry.k8s.io/external-dns/external-dns:v0.13.5 volumeMounts: - name: bluecatconfig mountPath: \"/etc/external-dns/\" readOnly: true args: - --log-level=debug - --source=service - --provider=bluecat - --txt-owner-id=bluecat-example - --bluecat-config-file=/etc/external-dns/bluecat.json EOF kubectl apply -f ~/bluecat.yml -n bluecat-example","title":"Deploy"},{"location":"tutorials/civo/","text":"Setting up ExternalDNS for Services on Civo \u00b6 This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using Civo DNS Manager. Make sure to use >0.13.5 version of ExternalDNS for this tutorial. Managing DNS with Civo \u00b6 If you want to learn about how to use Civo DNS Manager read the following tutorials: An Introduction to Managing DNS Get Civo Token \u00b6 Copy the token in the settings for your account The environment variable CIVO_TOKEN will be needed to run ExternalDNS with Civo. Deploy ExternalDNS \u00b6 Connect your kubectl client to the cluster you want to test ExternalDNS with. Then apply one of the following manifests file to deploy ExternalDNS. Manifest (for clusters without RBAC enabled) \u00b6 apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=civo env : - name : CIVO_TOKEN value : \"YOUR_CIVO_API_TOKEN\" Manifest (for clusters with RBAC enabled) \u00b6 apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=civo env : - name : CIVO_TOKEN value : \"YOUR_CIVO_API_TOKEN\" Deploying an Nginx Service \u00b6 Create a service file called \u2018nginx.yaml\u2019 with the following contents: apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 --- apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : my-app.example.com spec : selector : app : nginx type : LoadBalancer ports : - protocol : TCP port : 80 targetPort : 80 Note the annotation on the service; use the same hostname as the Civo DNS zone created above. ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records. Create the deployment and service: $ kubectl create -f nginx.yaml Depending where you run your service it can take a little while for your cloud provider to create an external IP for the service. Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize the Civo DNS records. Verifying Civo DNS records \u00b6 Check your Civo UI to view the records for your Civo DNS zone. Click on the zone for the one created above if a different domain was used. This should show the external IP address of the service as the A record for your domain. Cleanup \u00b6 Now that we have verified that ExternalDNS will automatically manage Civo DNS records, we can delete the tutorial\u2019s example: $ kubectl delete service -f nginx.yaml $ kubectl delete service -f externaldns.yaml","title":"Setting up ExternalDNS for Services on Civo"},{"location":"tutorials/civo/#setting-up-externaldns-for-services-on-civo","text":"This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using Civo DNS Manager. Make sure to use >0.13.5 version of ExternalDNS for this tutorial.","title":"Setting up ExternalDNS for Services on Civo"},{"location":"tutorials/civo/#managing-dns-with-civo","text":"If you want to learn about how to use Civo DNS Manager read the following tutorials: An Introduction to Managing DNS","title":"Managing DNS with Civo"},{"location":"tutorials/civo/#get-civo-token","text":"Copy the token in the settings for your account The environment variable CIVO_TOKEN will be needed to run ExternalDNS with Civo.","title":"Get Civo Token"},{"location":"tutorials/civo/#deploy-externaldns","text":"Connect your kubectl client to the cluster you want to test ExternalDNS with. Then apply one of the following manifests file to deploy ExternalDNS.","title":"Deploy ExternalDNS"},{"location":"tutorials/civo/#manifest-for-clusters-without-rbac-enabled","text":"apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=civo env : - name : CIVO_TOKEN value : \"YOUR_CIVO_API_TOKEN\"","title":"Manifest (for clusters without RBAC enabled)"},{"location":"tutorials/civo/#manifest-for-clusters-with-rbac-enabled","text":"apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=civo env : - name : CIVO_TOKEN value : \"YOUR_CIVO_API_TOKEN\"","title":"Manifest (for clusters with RBAC enabled)"},{"location":"tutorials/civo/#deploying-an-nginx-service","text":"Create a service file called \u2018nginx.yaml\u2019 with the following contents: apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 --- apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : my-app.example.com spec : selector : app : nginx type : LoadBalancer ports : - protocol : TCP port : 80 targetPort : 80 Note the annotation on the service; use the same hostname as the Civo DNS zone created above. ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records. Create the deployment and service: $ kubectl create -f nginx.yaml Depending where you run your service it can take a little while for your cloud provider to create an external IP for the service. Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize the Civo DNS records.","title":"Deploying an Nginx Service"},{"location":"tutorials/civo/#verifying-civo-dns-records","text":"Check your Civo UI to view the records for your Civo DNS zone. Click on the zone for the one created above if a different domain was used. This should show the external IP address of the service as the A record for your domain.","title":"Verifying Civo DNS records"},{"location":"tutorials/civo/#cleanup","text":"Now that we have verified that ExternalDNS will automatically manage Civo DNS records, we can delete the tutorial\u2019s example: $ kubectl delete service -f nginx.yaml $ kubectl delete service -f externaldns.yaml","title":"Cleanup"},{"location":"tutorials/cloudflare/","text":"Setting up ExternalDNS for Services on Cloudflare \u00b6 This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using Cloudflare DNS. Make sure to use >=0.4.2 version of ExternalDNS for this tutorial. Creating a Cloudflare DNS zone \u00b6 We highly recommend to read this tutorial if you haven\u2019t used Cloudflare before: Create a Cloudflare account and add a website Creating Cloudflare Credentials \u00b6 Snippet from Cloudflare - Getting Started : Cloudflare\u2019s API exposes the entire Cloudflare infrastructure via a standardized programmatic interface. Using Cloudflare\u2019s API, you can do just about anything you can do on cloudflare.com via the customer dashboard. The Cloudflare API is a RESTful API based on HTTPS requests and JSON responses. If you are registered with Cloudflare, you can obtain your API key from the bottom of the \u201cMy Account\u201d page, found here: Go to My account . API Token will be preferred for authentication if CF_API_TOKEN environment variable is set. Otherwise CF_API_KEY and CF_API_EMAIL should be set to run ExternalDNS with Cloudflare. You may provide the Cloudflare API token through a file by setting the CF_API_TOKEN=\"file:/path/to/token\" . When using API Token authentication, the token should be granted Zone Read , DNS Edit privileges, and access to All zones . If you would like to further restrict the API permissions to a specific zone (or zones), you also need to use the --zone-id-filter so that the underlying API requests only access the zones that you explicitly specify, as opposed to accessing all zones. Throttling \u00b6 Cloudflare API has a global rate limit of 1,200 requests per five minutes . Running several fast polling ExternalDNS instances in a given account can easily hit that limit. The AWS Provider docs has some recommendations that can be followed here too, but in particular, consider passing --cloudflare-dns-records-per-page with a high value (maximum is 5,000). Deploy ExternalDNS \u00b6 Connect your kubectl client to the cluster you want to test ExternalDNS with. Then apply one of the following manifests file to deploy ExternalDNS. Manifest (for clusters without RBAC enabled) \u00b6 apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --zone-id-filter=023e105f4ecef8ad9ca31a8372d0c353 # (optional) limit to a specific zone. - --provider=cloudflare - --cloudflare-proxied # (optional) enable the proxy feature of Cloudflare (DDOS protection, CDN...) - --cloudflare-dns-records-per-page=5000 # (optional) configure how many DNS records to fetch per request env : - name : CF_API_KEY value : \"YOUR_CLOUDFLARE_API_KEY\" - name : CF_API_EMAIL value : \"YOUR_CLOUDFLARE_EMAIL\" Manifest (for clusters with RBAC enabled) \u00b6 apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" , \"watch\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --zone-id-filter=023e105f4ecef8ad9ca31a8372d0c353 # (optional) limit to a specific zone. - --provider=cloudflare - --cloudflare-proxied # (optional) enable the proxy feature of Cloudflare (DDOS protection, CDN...) - --cloudflare-dns-records-per-page=5000 # (optional) configure how many DNS records to fetch per request env : - name : CF_API_KEY value : \"YOUR_CLOUDFLARE_API_KEY\" - name : CF_API_EMAIL value : \"YOUR_CLOUDFLARE_EMAIL\" Deploying an Nginx Service \u00b6 Create a service file called \u2018nginx.yaml\u2019 with the following contents: apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 --- apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : example.com external-dns.alpha.kubernetes.io/ttl : \"120\" #optional spec : selector : app : nginx type : LoadBalancer ports : - protocol : TCP port : 80 targetPort : 80 Note the annotation on the service; use the same hostname as the Cloudflare DNS zone created above. The annotation may also be a subdomain of the DNS zone (e.g. \u2018www.example.com\u2019). By setting the TTL annotation on the service, you have to pass a valid TTL, which must be 120 or above. This annotation is optional, if you won\u2019t set it, it will be 1 (automatic) which is 300. For Cloudflare proxied entries, set the TTL annotation to 1 (automatic), or do not set it. ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records. Create the deployment and service: $ kubectl create -f nginx.yaml Depending where you run your service it can take a little while for your cloud provider to create an external IP for the service. Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize the Cloudflare DNS records. Verifying Cloudflare DNS records \u00b6 Check your Cloudflare dashboard to view the records for your Cloudflare DNS zone. Substitute the zone for the one created above if a different domain was used. This should show the external IP address of the service as the A record for your domain. Cleanup \u00b6 Now that we have verified that ExternalDNS will automatically manage Cloudflare DNS records, we can delete the tutorial\u2019s example: $ kubectl delete -f nginx.yaml $ kubectl delete -f externaldns.yaml Setting cloudflare-proxied on a per-ingress basis \u00b6 Using the external-dns.alpha.kubernetes.io/cloudflare-proxied: \"true\" annotation on your ingress, you can specify if the proxy feature of Cloudflare should be enabled for that record. This setting will override the global --cloudflare-proxied setting.","title":"Setting up ExternalDNS for Services on Cloudflare"},{"location":"tutorials/cloudflare/#setting-up-externaldns-for-services-on-cloudflare","text":"This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using Cloudflare DNS. Make sure to use >=0.4.2 version of ExternalDNS for this tutorial.","title":"Setting up ExternalDNS for Services on Cloudflare"},{"location":"tutorials/cloudflare/#creating-a-cloudflare-dns-zone","text":"We highly recommend to read this tutorial if you haven\u2019t used Cloudflare before: Create a Cloudflare account and add a website","title":"Creating a Cloudflare DNS zone"},{"location":"tutorials/cloudflare/#creating-cloudflare-credentials","text":"Snippet from Cloudflare - Getting Started : Cloudflare\u2019s API exposes the entire Cloudflare infrastructure via a standardized programmatic interface. Using Cloudflare\u2019s API, you can do just about anything you can do on cloudflare.com via the customer dashboard. The Cloudflare API is a RESTful API based on HTTPS requests and JSON responses. If you are registered with Cloudflare, you can obtain your API key from the bottom of the \u201cMy Account\u201d page, found here: Go to My account . API Token will be preferred for authentication if CF_API_TOKEN environment variable is set. Otherwise CF_API_KEY and CF_API_EMAIL should be set to run ExternalDNS with Cloudflare. You may provide the Cloudflare API token through a file by setting the CF_API_TOKEN=\"file:/path/to/token\" . When using API Token authentication, the token should be granted Zone Read , DNS Edit privileges, and access to All zones . If you would like to further restrict the API permissions to a specific zone (or zones), you also need to use the --zone-id-filter so that the underlying API requests only access the zones that you explicitly specify, as opposed to accessing all zones.","title":"Creating Cloudflare Credentials"},{"location":"tutorials/cloudflare/#throttling","text":"Cloudflare API has a global rate limit of 1,200 requests per five minutes . Running several fast polling ExternalDNS instances in a given account can easily hit that limit. The AWS Provider docs has some recommendations that can be followed here too, but in particular, consider passing --cloudflare-dns-records-per-page with a high value (maximum is 5,000).","title":"Throttling"},{"location":"tutorials/cloudflare/#deploy-externaldns","text":"Connect your kubectl client to the cluster you want to test ExternalDNS with. Then apply one of the following manifests file to deploy ExternalDNS.","title":"Deploy ExternalDNS"},{"location":"tutorials/cloudflare/#manifest-for-clusters-without-rbac-enabled","text":"apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --zone-id-filter=023e105f4ecef8ad9ca31a8372d0c353 # (optional) limit to a specific zone. - --provider=cloudflare - --cloudflare-proxied # (optional) enable the proxy feature of Cloudflare (DDOS protection, CDN...) - --cloudflare-dns-records-per-page=5000 # (optional) configure how many DNS records to fetch per request env : - name : CF_API_KEY value : \"YOUR_CLOUDFLARE_API_KEY\" - name : CF_API_EMAIL value : \"YOUR_CLOUDFLARE_EMAIL\"","title":"Manifest (for clusters without RBAC enabled)"},{"location":"tutorials/cloudflare/#manifest-for-clusters-with-rbac-enabled","text":"apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" , \"watch\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --zone-id-filter=023e105f4ecef8ad9ca31a8372d0c353 # (optional) limit to a specific zone. - --provider=cloudflare - --cloudflare-proxied # (optional) enable the proxy feature of Cloudflare (DDOS protection, CDN...) - --cloudflare-dns-records-per-page=5000 # (optional) configure how many DNS records to fetch per request env : - name : CF_API_KEY value : \"YOUR_CLOUDFLARE_API_KEY\" - name : CF_API_EMAIL value : \"YOUR_CLOUDFLARE_EMAIL\"","title":"Manifest (for clusters with RBAC enabled)"},{"location":"tutorials/cloudflare/#deploying-an-nginx-service","text":"Create a service file called \u2018nginx.yaml\u2019 with the following contents: apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 --- apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : example.com external-dns.alpha.kubernetes.io/ttl : \"120\" #optional spec : selector : app : nginx type : LoadBalancer ports : - protocol : TCP port : 80 targetPort : 80 Note the annotation on the service; use the same hostname as the Cloudflare DNS zone created above. The annotation may also be a subdomain of the DNS zone (e.g. \u2018www.example.com\u2019). By setting the TTL annotation on the service, you have to pass a valid TTL, which must be 120 or above. This annotation is optional, if you won\u2019t set it, it will be 1 (automatic) which is 300. For Cloudflare proxied entries, set the TTL annotation to 1 (automatic), or do not set it. ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records. Create the deployment and service: $ kubectl create -f nginx.yaml Depending where you run your service it can take a little while for your cloud provider to create an external IP for the service. Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize the Cloudflare DNS records.","title":"Deploying an Nginx Service"},{"location":"tutorials/cloudflare/#verifying-cloudflare-dns-records","text":"Check your Cloudflare dashboard to view the records for your Cloudflare DNS zone. Substitute the zone for the one created above if a different domain was used. This should show the external IP address of the service as the A record for your domain.","title":"Verifying Cloudflare DNS records"},{"location":"tutorials/cloudflare/#cleanup","text":"Now that we have verified that ExternalDNS will automatically manage Cloudflare DNS records, we can delete the tutorial\u2019s example: $ kubectl delete -f nginx.yaml $ kubectl delete -f externaldns.yaml","title":"Cleanup"},{"location":"tutorials/cloudflare/#setting-cloudflare-proxied-on-a-per-ingress-basis","text":"Using the external-dns.alpha.kubernetes.io/cloudflare-proxied: \"true\" annotation on your ingress, you can specify if the proxy feature of Cloudflare should be enabled for that record. This setting will override the global --cloudflare-proxied setting.","title":"Setting cloudflare-proxied on a per-ingress basis"},{"location":"tutorials/contour/","text":"Setting up External DNS with Contour \u00b6 This tutorial describes how to configure External DNS to use the Contour HTTPProxy source. Using the HTTPProxy resource with External DNS requires Contour version 1.5 or greater. Example manifests for External DNS \u00b6 Without RBAC \u00b6 apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service - --source=ingress - --source=contour-httpproxy - --domain-filter=external-dns-test.my-org.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones - --provider=aws - --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization - --aws-zone-type=public # only look at public hosted zones (valid values are public, private or no value for both) - --registry=txt - --txt-owner-id=my-identifier With RBAC \u00b6 apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" ] - apiGroups : [ \"projectcontour.io\" ] resources : [ \"httpproxies\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service - --source=ingress - --source=contour-httpproxy - --domain-filter=external-dns-test.my-org.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones - --provider=aws - --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization - --aws-zone-type=public # only look at public hosted zones (valid values are public, private or no value for both) - --registry=txt - --txt-owner-id=my-identifier Verify External DNS works \u00b6 The following instructions are based on the Contour example workload . Install a sample service \u00b6 $ kubectl apply -f - < 2379/TCP 16m * Parameters should configure your own domain. \u201cexample.org\u201d is used in this example. After configuration done in values.yaml, you can install coredns chart. helm install --name my-coredns --values values.yaml stable/coredns Installing ExternalDNS \u00b6 Install external ExternalDNS \u00b6 ETCD_URLS is configured to etcd client service address. Manifest (for clusters without RBAC enabled) \u00b6 apiVersion : apps/v1 kind : Deployment metadata : name : external-dns namespace : kube-system spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=ingress - --provider=coredns - --log-level=debug # debug only env : - name : ETCD_URLS value : http://10.105.68.165:2379 Manifest (for clusters with RBAC enabled) \u00b6 --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : kube-system --- apiVersion : v1 kind : ServiceAccount metadata : name : external-dns namespace : kube-system --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns namespace : kube-system spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=ingress - --provider=coredns - --log-level=debug # debug only env : - name : ETCD_URLS value : http://10.105.68.165:2379 Enable the ingress controller \u00b6 You can use the ingress controller in minikube cluster. It needs to enable ingress addon in the cluster. minikube addons enable ingress Testing ingress example \u00b6 $ cat ingress.yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: nginx spec: ingressClassName: nginx rules: - host: nginx.example.org http: paths: - backend: serviceName: nginx servicePort: 80 $ kubectl apply -f ingress.yaml ingress.extensions \"nginx\" created Wait a moment until DNS has the ingress IP. The DNS service IP is from CoreDNS service. It is \u201cmy-coredns-coredns\u201d in this example. $ kubectl get svc my-coredns-coredns NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE my-coredns-coredns ClusterIP 10.100.4.143 53/UDP 12m $ kubectl get ingress NAME HOSTS ADDRESS PORTS AGE nginx nginx.example.org 10.0.2.15 80 2m $ kubectl run -it --rm --restart=Never --image=infoblox/dnstools:latest dnstools If you don't see a command prompt, try pressing enter. dnstools# dig @10.100.4.143 nginx.example.org +short 10.0.2.15 dnstools#","title":"Setting up ExternalDNS for CoreDNS with minikube"},{"location":"tutorials/coredns/#setting-up-externaldns-for-coredns-with-minikube","text":"This tutorial describes how to setup ExternalDNS for usage within a minikube cluster that makes use of CoreDNS and nginx ingress controller . You need to: * install CoreDNS with etcd enabled * install external-dns with coredns as a provider * enable ingress controller for the minikube cluster","title":"Setting up ExternalDNS for CoreDNS with minikube"},{"location":"tutorials/coredns/#creating-a-cluster","text":"minikube start","title":"Creating a cluster"},{"location":"tutorials/coredns/#installing-coredns-with-etcd-enabled","text":"Helm chart is used to install etcd and CoreDNS.","title":"Installing CoreDNS with etcd enabled"},{"location":"tutorials/coredns/#initializing-helm-chart","text":"helm init","title":"Initializing helm chart"},{"location":"tutorials/coredns/#installing-etcd","text":"etcd operator is used to manage etcd clusters. helm install stable/etcd-operator --name my-etcd-op etcd cluster is installed with example yaml from etcd operator website. kubectl apply -f https://raw.githubusercontent.com/coreos/etcd-operator/HEAD/example/example-etcd-cluster.yaml","title":"Installing etcd"},{"location":"tutorials/coredns/#installing-coredns","text":"In order to make CoreDNS work with etcd backend, values.yaml of the chart should be changed with corresponding configurations. wget https://raw.githubusercontent.com/helm/charts/HEAD/stable/coredns/values.yaml You need to edit/patch the file with below diff diff --git a/values.yaml b/values.yaml index 964e72b..e2fa934 100644 --- a/values.yaml +++ b/values.yaml @@ -27,12 +27,12 @@ service: rbac: # If true, create & use RBAC resources - create: false + create: true # Ignored if rbac.create is true serviceAccountName: default # isClusterService specifies whether chart should be deployed as cluster-service or normal k8s app. -isClusterService: true +isClusterService: false servers: - zones: @@ -51,6 +51,12 @@ servers: parameters: 0.0.0.0:9153 - name: proxy parameters: . /etc/resolv.conf + - name: etcd + parameters: example.org + configBlock: |- + stubzones + path /skydns + endpoint http://10.105.68.165:2379 # Complete example with all the options: # - zones: # the `zones` block can be left out entirely, defaults to \".\" Note : * IP address of etcd\u2019s endpoint should be get from etcd client service. It should be \u201cexample-etcd-cluster-client\u201d in this example. This IP address is used through this document for etcd endpoint configuration. $ kubectl get svc example-etcd-cluster-client NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE example-etcd-cluster-client ClusterIP 10.105.68.165 2379/TCP 16m * Parameters should configure your own domain. \u201cexample.org\u201d is used in this example. After configuration done in values.yaml, you can install coredns chart. helm install --name my-coredns --values values.yaml stable/coredns","title":"Installing CoreDNS"},{"location":"tutorials/coredns/#installing-externaldns","text":"","title":"Installing ExternalDNS"},{"location":"tutorials/coredns/#install-external-externaldns","text":"ETCD_URLS is configured to etcd client service address.","title":"Install external ExternalDNS"},{"location":"tutorials/coredns/#manifest-for-clusters-without-rbac-enabled","text":"apiVersion : apps/v1 kind : Deployment metadata : name : external-dns namespace : kube-system spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=ingress - --provider=coredns - --log-level=debug # debug only env : - name : ETCD_URLS value : http://10.105.68.165:2379","title":"Manifest (for clusters without RBAC enabled)"},{"location":"tutorials/coredns/#manifest-for-clusters-with-rbac-enabled","text":"--- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : kube-system --- apiVersion : v1 kind : ServiceAccount metadata : name : external-dns namespace : kube-system --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns namespace : kube-system spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=ingress - --provider=coredns - --log-level=debug # debug only env : - name : ETCD_URLS value : http://10.105.68.165:2379","title":"Manifest (for clusters with RBAC enabled)"},{"location":"tutorials/coredns/#enable-the-ingress-controller","text":"You can use the ingress controller in minikube cluster. It needs to enable ingress addon in the cluster. minikube addons enable ingress","title":"Enable the ingress controller"},{"location":"tutorials/coredns/#testing-ingress-example","text":"$ cat ingress.yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: nginx spec: ingressClassName: nginx rules: - host: nginx.example.org http: paths: - backend: serviceName: nginx servicePort: 80 $ kubectl apply -f ingress.yaml ingress.extensions \"nginx\" created Wait a moment until DNS has the ingress IP. The DNS service IP is from CoreDNS service. It is \u201cmy-coredns-coredns\u201d in this example. $ kubectl get svc my-coredns-coredns NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE my-coredns-coredns ClusterIP 10.100.4.143 53/UDP 12m $ kubectl get ingress NAME HOSTS ADDRESS PORTS AGE nginx nginx.example.org 10.0.2.15 80 2m $ kubectl run -it --rm --restart=Never --image=infoblox/dnstools:latest dnstools If you don't see a command prompt, try pressing enter. dnstools# dig @10.100.4.143 nginx.example.org +short 10.0.2.15 dnstools#","title":"Testing ingress example"},{"location":"tutorials/designate/","text":"Setting up ExternalDNS for Services on OpenStack Designate \u00b6 This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using OpenStack Designate DNS. Authenticating with OpenStack \u00b6 We are going to use OpenStack CLI - openstack utility, which is an umbrella application for most of OpenStack clients including designate . All OpenStack CLIs require authentication parameters to be provided. These parameters include: * URL of the OpenStack identity service ( keystone ) which is responsible for user authentication and also served as a registry for other OpenStack services. Designate endpoints must be registered in keystone in order to ExternalDNS and OpenStack CLI be able to find them. * OpenStack region name * User login name. * User project (tenant) name. * User domain (only when using keystone API v3) Although these parameters can be passed explicitly through the CLI flags, traditionally it is done by sourcing openrc file ( source ~/openrc ) that is a shell snippet that sets environment variables that all OpenStack CLI understand by convention. Recent versions of OpenStack Dashboard have a nice UI to download openrc file for both v2 and v3 auth protocols. Both protocols can be used with ExternalDNS. v3 is generally preferred over v2, but might not be available in some OpenStack installations. Installing OpenStack Designate \u00b6 Please refer to the Designate deployment tutorial for instructions on how to install and test Designate with BIND backend. You will be required to have admin rights in existing OpenStack installation to do this. One convenient way to get yourself an OpenStack installation to play with is to use DevStack . Creating DNS zones \u00b6 All domain names that are ExternalDNS is going to create must belong to one of DNS zones created in advance. Here is an example of how to create example.com DNS zone: $ openstack zone create --email dnsmaster@example.com example.com. It is important to manually create all the zones that are going to be used for kubernetes entities (ExternalDNS sources) before starting ExternalDNS. Deploy ExternalDNS \u00b6 Create a deployment file called externaldns.yaml with the following contents: Manifest (for clusters without RBAC enabled) \u00b6 apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=designate env : # values from openrc file - name : OS_AUTH_URL value : https://controller/identity/v3 - name : OS_REGION_NAME value : RegionOne - name : OS_USERNAME value : admin - name : OS_PASSWORD value : p@ssw0rd - name : OS_PROJECT_NAME value : demo - name : OS_USER_DOMAIN_NAME value : Default Manifest (for clusters with RBAC enabled) \u00b6 apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"watch\" , \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : selector : matchLabels : app : external-dns strategy : type : Recreate template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=designate env : # values from openrc file - name : OS_AUTH_URL value : https://controller/identity/v3 - name : OS_REGION_NAME value : RegionOne - name : OS_USERNAME value : admin - name : OS_PASSWORD value : p@ssw0rd - name : OS_PROJECT_NAME value : demo - name : OS_USER_DOMAIN_NAME value : Default Create the deployment for ExternalDNS: $ kubectl create -f externaldns.yaml Optional: Trust self-sign certificates \u00b6 If your OpenStack-Installation is configured with a self-sign certificate, you could extend the pod.spec with following secret-mount: volumeMounts : - mountPath : /etc/ssl/certs/ name : cacerts volumes : - name : cacerts secret : defaultMode : 420 secretName : self-sign-certs content of the secret self-sign-certs must be the certificate/chain in PEM format. Deploying an Nginx Service \u00b6 Create a service file called \u2018nginx.yaml\u2019 with the following contents: apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 --- apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : my-app.example.com spec : selector : app : nginx type : LoadBalancer ports : - protocol : TCP port : 80 targetPort : 80 Note the annotation on the service; use the same hostname as the DNS zone created above. ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records. Create the deployment and service: $ kubectl create -f nginx.yaml Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and notify Designate, which in turn synchronize DNS records with underlying DNS server backend. Verifying DNS records \u00b6 To verify that DNS record was indeed created, you can use the following command: $ openstack recordset list example.com. There should be a record for my-app.example.com having ACTIVE status. And of course, the ultimate method to verify is to issue a DNS query: $ dig my-app.example.com @controller Cleanup \u00b6 Now that we have verified that ExternalDNS created all DNS records, we can delete the tutorial\u2019s example: $ kubectl delete service -f nginx.yaml $ kubectl delete service -f externaldns.yaml","title":"Setting up ExternalDNS for Services on OpenStack Designate"},{"location":"tutorials/designate/#setting-up-externaldns-for-services-on-openstack-designate","text":"This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using OpenStack Designate DNS.","title":"Setting up ExternalDNS for Services on OpenStack Designate"},{"location":"tutorials/designate/#authenticating-with-openstack","text":"We are going to use OpenStack CLI - openstack utility, which is an umbrella application for most of OpenStack clients including designate . All OpenStack CLIs require authentication parameters to be provided. These parameters include: * URL of the OpenStack identity service ( keystone ) which is responsible for user authentication and also served as a registry for other OpenStack services. Designate endpoints must be registered in keystone in order to ExternalDNS and OpenStack CLI be able to find them. * OpenStack region name * User login name. * User project (tenant) name. * User domain (only when using keystone API v3) Although these parameters can be passed explicitly through the CLI flags, traditionally it is done by sourcing openrc file ( source ~/openrc ) that is a shell snippet that sets environment variables that all OpenStack CLI understand by convention. Recent versions of OpenStack Dashboard have a nice UI to download openrc file for both v2 and v3 auth protocols. Both protocols can be used with ExternalDNS. v3 is generally preferred over v2, but might not be available in some OpenStack installations.","title":"Authenticating with OpenStack"},{"location":"tutorials/designate/#installing-openstack-designate","text":"Please refer to the Designate deployment tutorial for instructions on how to install and test Designate with BIND backend. You will be required to have admin rights in existing OpenStack installation to do this. One convenient way to get yourself an OpenStack installation to play with is to use DevStack .","title":"Installing OpenStack Designate"},{"location":"tutorials/designate/#creating-dns-zones","text":"All domain names that are ExternalDNS is going to create must belong to one of DNS zones created in advance. Here is an example of how to create example.com DNS zone: $ openstack zone create --email dnsmaster@example.com example.com. It is important to manually create all the zones that are going to be used for kubernetes entities (ExternalDNS sources) before starting ExternalDNS.","title":"Creating DNS zones"},{"location":"tutorials/designate/#deploy-externaldns","text":"Create a deployment file called externaldns.yaml with the following contents:","title":"Deploy ExternalDNS"},{"location":"tutorials/designate/#manifest-for-clusters-without-rbac-enabled","text":"apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=designate env : # values from openrc file - name : OS_AUTH_URL value : https://controller/identity/v3 - name : OS_REGION_NAME value : RegionOne - name : OS_USERNAME value : admin - name : OS_PASSWORD value : p@ssw0rd - name : OS_PROJECT_NAME value : demo - name : OS_USER_DOMAIN_NAME value : Default","title":"Manifest (for clusters without RBAC enabled)"},{"location":"tutorials/designate/#manifest-for-clusters-with-rbac-enabled","text":"apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"watch\" , \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : selector : matchLabels : app : external-dns strategy : type : Recreate template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=designate env : # values from openrc file - name : OS_AUTH_URL value : https://controller/identity/v3 - name : OS_REGION_NAME value : RegionOne - name : OS_USERNAME value : admin - name : OS_PASSWORD value : p@ssw0rd - name : OS_PROJECT_NAME value : demo - name : OS_USER_DOMAIN_NAME value : Default Create the deployment for ExternalDNS: $ kubectl create -f externaldns.yaml","title":"Manifest (for clusters with RBAC enabled)"},{"location":"tutorials/designate/#optional-trust-self-sign-certificates","text":"If your OpenStack-Installation is configured with a self-sign certificate, you could extend the pod.spec with following secret-mount: volumeMounts : - mountPath : /etc/ssl/certs/ name : cacerts volumes : - name : cacerts secret : defaultMode : 420 secretName : self-sign-certs content of the secret self-sign-certs must be the certificate/chain in PEM format.","title":"Optional: Trust self-sign certificates"},{"location":"tutorials/designate/#deploying-an-nginx-service","text":"Create a service file called \u2018nginx.yaml\u2019 with the following contents: apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 --- apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : my-app.example.com spec : selector : app : nginx type : LoadBalancer ports : - protocol : TCP port : 80 targetPort : 80 Note the annotation on the service; use the same hostname as the DNS zone created above. ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records. Create the deployment and service: $ kubectl create -f nginx.yaml Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and notify Designate, which in turn synchronize DNS records with underlying DNS server backend.","title":"Deploying an Nginx Service"},{"location":"tutorials/designate/#verifying-dns-records","text":"To verify that DNS record was indeed created, you can use the following command: $ openstack recordset list example.com. There should be a record for my-app.example.com having ACTIVE status. And of course, the ultimate method to verify is to issue a DNS query: $ dig my-app.example.com @controller","title":"Verifying DNS records"},{"location":"tutorials/designate/#cleanup","text":"Now that we have verified that ExternalDNS created all DNS records, we can delete the tutorial\u2019s example: $ kubectl delete service -f nginx.yaml $ kubectl delete service -f externaldns.yaml","title":"Cleanup"},{"location":"tutorials/digitalocean/","text":"Setting up ExternalDNS for Services on DigitalOcean \u00b6 This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using DigitalOcean DNS. Make sure to use >=0.4.2 version of ExternalDNS for this tutorial. Creating a DigitalOcean DNS zone \u00b6 If you want to learn about how to use DigitalOcean\u2019s DNS service read the following tutorial series: An Introduction to Managing DNS , and specifically How To Set Up a Host Name with DigitalOcean DNS Create a new DNS zone where you want to create your records in. Let\u2019s use example.com as an example here. Creating DigitalOcean Credentials \u00b6 Generate a new personal token by going to the API settings or follow How To Use the DigitalOcean API v2 if you need more information. Give the token a name and choose read and write access. The token needs to be passed to ExternalDNS so make a note of it for later use. The environment variable DO_TOKEN will be needed to run ExternalDNS with DigitalOcean. Deploy ExternalDNS \u00b6 Connect your kubectl client to the cluster you want to test ExternalDNS with. Then apply one of the following manifests file to deploy ExternalDNS. Manifest (for clusters without RBAC enabled) \u00b6 apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : replicas : 1 selector : matchLabels : app : external-dns strategy : type : Recreate template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=digitalocean env : - name : DO_TOKEN value : \"YOUR_DIGITALOCEAN_API_KEY\" Manifest (for clusters with RBAC enabled) \u00b6 apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : replicas : 1 selector : matchLabels : app : external-dns strategy : type : Recreate template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=digitalocean env : - name : DO_TOKEN value : \"YOUR_DIGITALOCEAN_API_KEY\" Deploying an Nginx Service \u00b6 Create a service file called \u2018nginx.yaml\u2019 with the following contents: apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : replicas : 1 selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 --- apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : my-app.example.com spec : selector : app : nginx type : LoadBalancer ports : - protocol : TCP port : 80 targetPort : 80 Note the annotation on the service; use the same hostname as the DigitalOcean DNS zone created above. ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records. Create the deployment and service: $ kubectl create -f nginx.yaml Depending where you run your service it can take a little while for your cloud provider to create an external IP for the service. Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize the DigitalOcean DNS records. Verifying DigitalOcean DNS records \u00b6 Check your DigitalOcean UI to view the records for your DigitalOcean DNS zone. Click on the zone for the one created above if a different domain was used. This should show the external IP address of the service as the A record for your domain. Cleanup \u00b6 Now that we have verified that ExternalDNS will automatically manage DigitalOcean DNS records, we can delete the tutorial\u2019s example: $ kubectl delete service -f nginx.yaml $ kubectl delete service -f externaldns.yaml Advanced Usage \u00b6 API Page Size \u00b6 If you have a large number of domains and/or records within a domain, you may encounter API rate limiting because of the number of API calls that external-dns must make to the DigitalOcean API to retrieve the current DNS configuration during every reconciliation loop. If this is the case, use the --digitalocean-api-page-size option to increase the size of the pages used when querying the DigitalOcean API. (Note: external-dns uses a default of 50.)","title":"Setting up ExternalDNS for Services on DigitalOcean"},{"location":"tutorials/digitalocean/#setting-up-externaldns-for-services-on-digitalocean","text":"This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using DigitalOcean DNS. Make sure to use >=0.4.2 version of ExternalDNS for this tutorial.","title":"Setting up ExternalDNS for Services on DigitalOcean"},{"location":"tutorials/digitalocean/#creating-a-digitalocean-dns-zone","text":"If you want to learn about how to use DigitalOcean\u2019s DNS service read the following tutorial series: An Introduction to Managing DNS , and specifically How To Set Up a Host Name with DigitalOcean DNS Create a new DNS zone where you want to create your records in. Let\u2019s use example.com as an example here.","title":"Creating a DigitalOcean DNS zone"},{"location":"tutorials/digitalocean/#creating-digitalocean-credentials","text":"Generate a new personal token by going to the API settings or follow How To Use the DigitalOcean API v2 if you need more information. Give the token a name and choose read and write access. The token needs to be passed to ExternalDNS so make a note of it for later use. The environment variable DO_TOKEN will be needed to run ExternalDNS with DigitalOcean.","title":"Creating DigitalOcean Credentials"},{"location":"tutorials/digitalocean/#deploy-externaldns","text":"Connect your kubectl client to the cluster you want to test ExternalDNS with. Then apply one of the following manifests file to deploy ExternalDNS.","title":"Deploy ExternalDNS"},{"location":"tutorials/digitalocean/#manifest-for-clusters-without-rbac-enabled","text":"apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : replicas : 1 selector : matchLabels : app : external-dns strategy : type : Recreate template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=digitalocean env : - name : DO_TOKEN value : \"YOUR_DIGITALOCEAN_API_KEY\"","title":"Manifest (for clusters without RBAC enabled)"},{"location":"tutorials/digitalocean/#manifest-for-clusters-with-rbac-enabled","text":"apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : replicas : 1 selector : matchLabels : app : external-dns strategy : type : Recreate template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=digitalocean env : - name : DO_TOKEN value : \"YOUR_DIGITALOCEAN_API_KEY\"","title":"Manifest (for clusters with RBAC enabled)"},{"location":"tutorials/digitalocean/#deploying-an-nginx-service","text":"Create a service file called \u2018nginx.yaml\u2019 with the following contents: apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : replicas : 1 selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 --- apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : my-app.example.com spec : selector : app : nginx type : LoadBalancer ports : - protocol : TCP port : 80 targetPort : 80 Note the annotation on the service; use the same hostname as the DigitalOcean DNS zone created above. ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records. Create the deployment and service: $ kubectl create -f nginx.yaml Depending where you run your service it can take a little while for your cloud provider to create an external IP for the service. Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize the DigitalOcean DNS records.","title":"Deploying an Nginx Service"},{"location":"tutorials/digitalocean/#verifying-digitalocean-dns-records","text":"Check your DigitalOcean UI to view the records for your DigitalOcean DNS zone. Click on the zone for the one created above if a different domain was used. This should show the external IP address of the service as the A record for your domain.","title":"Verifying DigitalOcean DNS records"},{"location":"tutorials/digitalocean/#cleanup","text":"Now that we have verified that ExternalDNS will automatically manage DigitalOcean DNS records, we can delete the tutorial\u2019s example: $ kubectl delete service -f nginx.yaml $ kubectl delete service -f externaldns.yaml","title":"Cleanup"},{"location":"tutorials/digitalocean/#advanced-usage","text":"","title":"Advanced Usage"},{"location":"tutorials/digitalocean/#api-page-size","text":"If you have a large number of domains and/or records within a domain, you may encounter API rate limiting because of the number of API calls that external-dns must make to the DigitalOcean API to retrieve the current DNS configuration during every reconciliation loop. If this is the case, use the --digitalocean-api-page-size option to increase the size of the pages used when querying the DigitalOcean API. (Note: external-dns uses a default of 50.)","title":"API Page Size"},{"location":"tutorials/dnsimple/","text":"Setting up ExternalDNS for Services on DNSimple \u00b6 This tutorial describes how to setup ExternalDNS for usage with DNSimple. Make sure to use >=0.4.6 version of ExternalDNS for this tutorial. Created a DNSimple API Access Token \u00b6 A DNSimple API access token can be acquired by following the provided documentation from DNSimple The environment variable DNSIMPLE_OAUTH must be set to the API token generated for to run ExternalDNS with DNSimple. Deploy ExternalDNS \u00b6 Connect your kubectl client to the cluster you want to test ExternalDNS with. Then apply one of the following manifests file to deploy ExternalDNS. Manifest (for clusters without RBAC enabled) \u00b6 apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone you create in DNSimple. - --provider=dnsimple - --registry=txt env : - name : DNSIMPLE_OAUTH value : \"YOUR_DNSIMPLE_API_KEY\" Manifest (for clusters with RBAC enabled) \u00b6 apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone you create in DNSimple. - --provider=dnsimple - --registry=txt env : - name : DNSIMPLE_OAUTH value : \"YOUR_DNSIMPLE_API_KEY\" Deploying an Nginx Service \u00b6 Create a service file called \u2018nginx.yaml\u2019 with the following contents: apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 --- apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : validate-external-dns.example.com spec : selector : app : nginx type : LoadBalancer ports : - protocol : TCP port : 80 targetPort : 80 Note the annotation on the service; use the same hostname as the DNSimple DNS zone created above. The annotation may also be a subdomain of the DNS zone (e.g. \u2018www.example.com\u2019). ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records. Create the deployment and service: $ kubectl create -f nginx.yaml Depending where you run your service it can take a little while for your cloud provider to create an external IP for the service. Check the status by running kubectl get services nginx . If the EXTERNAL-IP field shows an address, the service is ready to be accessed externally. Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize the DNSimple DNS records. Verifying DNSimple DNS records \u00b6 Getting your DNSimple Account ID \u00b6 If you do not know your DNSimple account ID it can be acquired using the whoami endpoint from the DNSimple Identity API curl -H \"Authorization: Bearer $DNSIMPLE_ACCOUNT_TOKEN \" \\ -H 'Accept: application/json' \\ https://api.dnsimple.com/v2/whoami { \"data\" : { \"user\" : null, \"account\" : { \"id\" : 1 , \"email\" : \"example-account@example.com\" , \"plan_identifier\" : \"dnsimple-professional\" , \"created_at\" : \"2015-09-18T23:04:37Z\" , \"updated_at\" : \"2016-06-09T20:03:39Z\" } } } Looking at the DNSimple Dashboard \u00b6 You can view your DNSimple Record Editor at https://dnsimple.com/a/YOUR_ACCOUNT_ID/domains/example.com/records. Ensure you substitute the value YOUR_ACCOUNT_ID with the ID of your DNSimple account and example.com with the correct domain that you used during validation. Using the DNSimple Zone Records API \u00b6 This approach allows for you to use the DNSimple List records for a zone endpoint to verify the creation of the A and TXT record. Ensure you substitute the value YOUR_ACCOUNT_ID with the ID of your DNSimple account and example.com with the correct domain that you used during validation. curl -H \"Authorization: Bearer $DNSIMPLE_ACCOUNT_TOKEN \" \\ -H 'Accept: application/json' \\ 'https://api.dnsimple.com/v2/YOUR_ACCOUNT_ID/zones/example.com/records&name=validate-external-dns' Clean up \u00b6 Now that we have verified that ExternalDNS will automatically manage DNSimple DNS records, we can delete the tutorial\u2019s example: $ kubectl delete -f nginx.yaml $ kubectl delete -f externaldns.yaml Deleting Created Records \u00b6 The created records can be deleted using the record IDs from the verification step and the Delete a zone record endpoint.","title":"Setting up ExternalDNS for Services on DNSimple"},{"location":"tutorials/dnsimple/#setting-up-externaldns-for-services-on-dnsimple","text":"This tutorial describes how to setup ExternalDNS for usage with DNSimple. Make sure to use >=0.4.6 version of ExternalDNS for this tutorial.","title":"Setting up ExternalDNS for Services on DNSimple"},{"location":"tutorials/dnsimple/#created-a-dnsimple-api-access-token","text":"A DNSimple API access token can be acquired by following the provided documentation from DNSimple The environment variable DNSIMPLE_OAUTH must be set to the API token generated for to run ExternalDNS with DNSimple.","title":"Created a DNSimple API Access Token"},{"location":"tutorials/dnsimple/#deploy-externaldns","text":"Connect your kubectl client to the cluster you want to test ExternalDNS with. Then apply one of the following manifests file to deploy ExternalDNS.","title":"Deploy ExternalDNS"},{"location":"tutorials/dnsimple/#manifest-for-clusters-without-rbac-enabled","text":"apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone you create in DNSimple. - --provider=dnsimple - --registry=txt env : - name : DNSIMPLE_OAUTH value : \"YOUR_DNSIMPLE_API_KEY\"","title":"Manifest (for clusters without RBAC enabled)"},{"location":"tutorials/dnsimple/#manifest-for-clusters-with-rbac-enabled","text":"apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone you create in DNSimple. - --provider=dnsimple - --registry=txt env : - name : DNSIMPLE_OAUTH value : \"YOUR_DNSIMPLE_API_KEY\"","title":"Manifest (for clusters with RBAC enabled)"},{"location":"tutorials/dnsimple/#deploying-an-nginx-service","text":"Create a service file called \u2018nginx.yaml\u2019 with the following contents: apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 --- apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : validate-external-dns.example.com spec : selector : app : nginx type : LoadBalancer ports : - protocol : TCP port : 80 targetPort : 80 Note the annotation on the service; use the same hostname as the DNSimple DNS zone created above. The annotation may also be a subdomain of the DNS zone (e.g. \u2018www.example.com\u2019). ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records. Create the deployment and service: $ kubectl create -f nginx.yaml Depending where you run your service it can take a little while for your cloud provider to create an external IP for the service. Check the status by running kubectl get services nginx . If the EXTERNAL-IP field shows an address, the service is ready to be accessed externally. Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize the DNSimple DNS records.","title":"Deploying an Nginx Service"},{"location":"tutorials/dnsimple/#verifying-dnsimple-dns-records","text":"","title":"Verifying DNSimple DNS records"},{"location":"tutorials/dnsimple/#getting-your-dnsimple-account-id","text":"If you do not know your DNSimple account ID it can be acquired using the whoami endpoint from the DNSimple Identity API curl -H \"Authorization: Bearer $DNSIMPLE_ACCOUNT_TOKEN \" \\ -H 'Accept: application/json' \\ https://api.dnsimple.com/v2/whoami { \"data\" : { \"user\" : null, \"account\" : { \"id\" : 1 , \"email\" : \"example-account@example.com\" , \"plan_identifier\" : \"dnsimple-professional\" , \"created_at\" : \"2015-09-18T23:04:37Z\" , \"updated_at\" : \"2016-06-09T20:03:39Z\" } } }","title":"Getting your DNSimple Account ID"},{"location":"tutorials/dnsimple/#looking-at-the-dnsimple-dashboard","text":"You can view your DNSimple Record Editor at https://dnsimple.com/a/YOUR_ACCOUNT_ID/domains/example.com/records. Ensure you substitute the value YOUR_ACCOUNT_ID with the ID of your DNSimple account and example.com with the correct domain that you used during validation.","title":"Looking at the DNSimple Dashboard"},{"location":"tutorials/dnsimple/#using-the-dnsimple-zone-records-api","text":"This approach allows for you to use the DNSimple List records for a zone endpoint to verify the creation of the A and TXT record. Ensure you substitute the value YOUR_ACCOUNT_ID with the ID of your DNSimple account and example.com with the correct domain that you used during validation. curl -H \"Authorization: Bearer $DNSIMPLE_ACCOUNT_TOKEN \" \\ -H 'Accept: application/json' \\ 'https://api.dnsimple.com/v2/YOUR_ACCOUNT_ID/zones/example.com/records&name=validate-external-dns'","title":"Using the DNSimple Zone Records API"},{"location":"tutorials/dnsimple/#clean-up","text":"Now that we have verified that ExternalDNS will automatically manage DNSimple DNS records, we can delete the tutorial\u2019s example: $ kubectl delete -f nginx.yaml $ kubectl delete -f externaldns.yaml","title":"Clean up"},{"location":"tutorials/dnsimple/#deleting-created-records","text":"The created records can be deleted using the record IDs from the verification step and the Delete a zone record endpoint.","title":"Deleting Created Records"},{"location":"tutorials/dyn/","text":"Setting up ExternalDNS for Dyn \u00b6 Creating a Dyn Configuration Secret \u00b6 For ExternalDNS to access the Dyn API, create a Kubernetes secret. To create the secret: $ kubectl create secret generic external-dns \\ --from-literal=EXTERNAL_DNS_DYN_CUSTOMER_NAME=${DYN_CUSTOMER_NAME} \\ --from-literal=EXTERNAL_DNS_DYN_USERNAME=${DYN_USERNAME} \\ --from-literal=EXTERNAL_DNS_DYN_PASSWORD=${DYN_PASSWORD} The credentials are the same ones created during account registration. As best practise, you are advised to create an API-only user that is entitled to only the zones intended to be changed by ExternalDNS Deploy ExternalDNS \u00b6 The rest of this tutorial assumes you own example.com domain and your DNS provider is Dyn. Change example.com with a domain/zone that you really own. In case of the dyn provider, the flag --zone-id-filter is mandatory as it specifies which zones to scan for records. Without it Create a deployment file called externaldns.yaml with the following contents: apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=ingress - --txt-prefix=_d - --namespace=example - --zone-id-filter=example.com - --domain-filter=example.com - --provider=dyn env : - name : EXTERNAL_DNS_DYN_CUSTOMER_NAME valueFrom : secretKeyRef : name : external-dns key : EXTERNAL_DNS_DYN_CUSTOMER_NAME - name : EXTERNAL_DNS_DYN_USERNAME valueFrom : secretKeyRef : name : external-dns key : EXTERNAL_DNS_DYN_USERNAME - name : EXTERNAL_DNS_DYN_PASSWORD valueFrom : secretKeyRef : name : external-dns key : EXTERNAL_DNS_DYN_PASSWORD EOF As we\u2019ll be creating an Ingress resource, you need --txt-prefix=_d as a CNAME cannot coexist with a TXT record. You can change the prefix to any valid start of a FQDN. Create the deployment for ExternalDNS: $ kubectl create -f externaldns.yaml Running a locally build version \u00b6 If you just want to test ExternalDNS in dry-run mode locally without doing the above deployment you can also do it. Make sure your kubectl is configured correctly . Assuming you have the sources, build and run it like so: make # output skipped ./build/external-dns \\ --provider = dyn \\ --dyn-customer-name = ${ DYN_CUSTOMER_NAME } \\ --dyn-username = ${ DYN_USERNAME } \\ --dyn-password = ${ DYN_PASSWORD } \\ --domain-filter = example.com \\ --zone-id-filter = example.com \\ --namespace = example \\ --log-level = debug \\ --txt-prefix = _ \\ --dry-run = true INFO [ 0000 ] running in dry-run mode. No changes to DNS records will be made. INFO [ 0000 ] Connected to cluster at https://some-k8s-cluster.example.com INFO [ 0001 ] Zones: [ example.com ] # output skipped Having --dry-run=true and --log-level=debug is a great way to see exactly what DynamicDNS is doing or is about to do. Deploying an Ingress Resource \u00b6 Create a file called \u2018test-ingress.yaml\u2019 with the following contents: apiVersion : networking.k8s.io/v1 kind : Ingress metadata : name : test-ingress namespace : example spec : rules : - host : test-ingress.example.com http : paths : - backend : service : name : my-awesome-service port : number : 8080 pathType : Prefix As the DNS name test-ingress.example.com matches the filter, external-dns will create two records: a CNAME for test-ingress.example.com and TXT for _dtest-ingress.example.com. Create the Ingress: $ kubectl create -f test-ingress.yaml By default external-dns scans for changes every minute so give it some time to catch up with the Verifying Dyn DNS records \u00b6 Login to the console at https://portal.dynect.net/login/ and verify records are created Clean up \u00b6 Login to the console at https://portal.dynect.net/login/ and delete the records created. Alternatively, just delete the sample Ingress resources and external-dns will delete the records.","title":"Setting up ExternalDNS for Dyn"},{"location":"tutorials/dyn/#setting-up-externaldns-for-dyn","text":"","title":"Setting up ExternalDNS for Dyn"},{"location":"tutorials/dyn/#creating-a-dyn-configuration-secret","text":"For ExternalDNS to access the Dyn API, create a Kubernetes secret. To create the secret: $ kubectl create secret generic external-dns \\ --from-literal=EXTERNAL_DNS_DYN_CUSTOMER_NAME=${DYN_CUSTOMER_NAME} \\ --from-literal=EXTERNAL_DNS_DYN_USERNAME=${DYN_USERNAME} \\ --from-literal=EXTERNAL_DNS_DYN_PASSWORD=${DYN_PASSWORD} The credentials are the same ones created during account registration. As best practise, you are advised to create an API-only user that is entitled to only the zones intended to be changed by ExternalDNS","title":"Creating a Dyn Configuration Secret"},{"location":"tutorials/dyn/#deploy-externaldns","text":"The rest of this tutorial assumes you own example.com domain and your DNS provider is Dyn. Change example.com with a domain/zone that you really own. In case of the dyn provider, the flag --zone-id-filter is mandatory as it specifies which zones to scan for records. Without it Create a deployment file called externaldns.yaml with the following contents: apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=ingress - --txt-prefix=_d - --namespace=example - --zone-id-filter=example.com - --domain-filter=example.com - --provider=dyn env : - name : EXTERNAL_DNS_DYN_CUSTOMER_NAME valueFrom : secretKeyRef : name : external-dns key : EXTERNAL_DNS_DYN_CUSTOMER_NAME - name : EXTERNAL_DNS_DYN_USERNAME valueFrom : secretKeyRef : name : external-dns key : EXTERNAL_DNS_DYN_USERNAME - name : EXTERNAL_DNS_DYN_PASSWORD valueFrom : secretKeyRef : name : external-dns key : EXTERNAL_DNS_DYN_PASSWORD EOF As we\u2019ll be creating an Ingress resource, you need --txt-prefix=_d as a CNAME cannot coexist with a TXT record. You can change the prefix to any valid start of a FQDN. Create the deployment for ExternalDNS: $ kubectl create -f externaldns.yaml","title":"Deploy ExternalDNS"},{"location":"tutorials/dyn/#running-a-locally-build-version","text":"If you just want to test ExternalDNS in dry-run mode locally without doing the above deployment you can also do it. Make sure your kubectl is configured correctly . Assuming you have the sources, build and run it like so: make # output skipped ./build/external-dns \\ --provider = dyn \\ --dyn-customer-name = ${ DYN_CUSTOMER_NAME } \\ --dyn-username = ${ DYN_USERNAME } \\ --dyn-password = ${ DYN_PASSWORD } \\ --domain-filter = example.com \\ --zone-id-filter = example.com \\ --namespace = example \\ --log-level = debug \\ --txt-prefix = _ \\ --dry-run = true INFO [ 0000 ] running in dry-run mode. No changes to DNS records will be made. INFO [ 0000 ] Connected to cluster at https://some-k8s-cluster.example.com INFO [ 0001 ] Zones: [ example.com ] # output skipped Having --dry-run=true and --log-level=debug is a great way to see exactly what DynamicDNS is doing or is about to do.","title":"Running a locally build version"},{"location":"tutorials/dyn/#deploying-an-ingress-resource","text":"Create a file called \u2018test-ingress.yaml\u2019 with the following contents: apiVersion : networking.k8s.io/v1 kind : Ingress metadata : name : test-ingress namespace : example spec : rules : - host : test-ingress.example.com http : paths : - backend : service : name : my-awesome-service port : number : 8080 pathType : Prefix As the DNS name test-ingress.example.com matches the filter, external-dns will create two records: a CNAME for test-ingress.example.com and TXT for _dtest-ingress.example.com. Create the Ingress: $ kubectl create -f test-ingress.yaml By default external-dns scans for changes every minute so give it some time to catch up with the","title":"Deploying an Ingress Resource"},{"location":"tutorials/dyn/#verifying-dyn-dns-records","text":"Login to the console at https://portal.dynect.net/login/ and verify records are created","title":"Verifying Dyn DNS records"},{"location":"tutorials/dyn/#clean-up","text":"Login to the console at https://portal.dynect.net/login/ and delete the records created. Alternatively, just delete the sample Ingress resources and external-dns will delete the records.","title":"Clean up"},{"location":"tutorials/exoscale/","text":"Setting up ExternalDNS for Exoscale \u00b6 Prerequisites \u00b6 Exoscale provider support was added via this PR , thus you need to use external-dns v0.5.5. The Exoscale provider expects that your Exoscale zones, you wish to add records to, already exists and are configured correctly. It does not add, remove or configure new zones in anyway. To do this please refer to the Exoscale DNS documentation . Additionally you will have to provide the Exoscale\u2026: API Key API Secret Elastic IP address, to access the workers Deployment \u00b6 Deploying external DNS for Exoscale is actually nearly identical to deploying it for other providers. This is what a sample deployment.yaml looks like: apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : # Only use if you're also using RBAC # serviceAccountName: external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=ingress # or service or both - --provider=exoscale - --domain-filter={{ my-domain }} - --policy=sync # if you want DNS entries to get deleted as well - --txt-owner-id={{ owner-id-for-this-external-dns }} - --exoscale-apikey={{ api-key}} - --exoscale-apisecret={{ api-secret }} # - --exoscale-apizone={{ api-zone }} # - --exoscale-apienv={{ api-env }} Optional arguments --exoscale-apizone and --exoscale-apienv define Exoscale API Zone (default ch-gva-2 ) and Exoscale API environment (default api , can be used to target non-production API server) respectively. RBAC \u00b6 If your cluster is RBAC enabled, you also need to setup the following, before you can run external-dns: apiVersion : v1 kind : ServiceAccount metadata : name : external-dns namespace : default --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default Testing and Verification \u00b6 Important! : Remember to change example.com with your own domain throughout the following text. Spin up a simple nginx HTTP server with the following spec ( kubectl apply -f ): apiVersion : networking.k8s.io/v1 kind : Ingress metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/target : {{ Elastic-IP-address }} spec : ingressClassName : nginx rules : - host : via-ingress.example.com http : paths : - backend : service : name : \"nginx\" port : number : 80 path : / pathType : Prefix --- apiVersion : v1 kind : Service metadata : name : nginx spec : ports : - port : 80 targetPort : 80 selector : app : nginx --- apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 Important! : Don\u2019t run dig, nslookup or similar immediately (until you\u2019ve confirmed the record exists). You\u2019ll get hit by negative DNS caching , which is hard to flush. Wait about 30s-1m (interval for external-dns to kick in), then check Exoscales portal \u2026 via-ingress.example.com should appear as a A and TXT record with your Elastic-IP-address.","title":"Setting up ExternalDNS for Exoscale"},{"location":"tutorials/exoscale/#setting-up-externaldns-for-exoscale","text":"","title":"Setting up ExternalDNS for Exoscale"},{"location":"tutorials/exoscale/#prerequisites","text":"Exoscale provider support was added via this PR , thus you need to use external-dns v0.5.5. The Exoscale provider expects that your Exoscale zones, you wish to add records to, already exists and are configured correctly. It does not add, remove or configure new zones in anyway. To do this please refer to the Exoscale DNS documentation . Additionally you will have to provide the Exoscale\u2026: API Key API Secret Elastic IP address, to access the workers","title":"Prerequisites"},{"location":"tutorials/exoscale/#deployment","text":"Deploying external DNS for Exoscale is actually nearly identical to deploying it for other providers. This is what a sample deployment.yaml looks like: apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : # Only use if you're also using RBAC # serviceAccountName: external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=ingress # or service or both - --provider=exoscale - --domain-filter={{ my-domain }} - --policy=sync # if you want DNS entries to get deleted as well - --txt-owner-id={{ owner-id-for-this-external-dns }} - --exoscale-apikey={{ api-key}} - --exoscale-apisecret={{ api-secret }} # - --exoscale-apizone={{ api-zone }} # - --exoscale-apienv={{ api-env }} Optional arguments --exoscale-apizone and --exoscale-apienv define Exoscale API Zone (default ch-gva-2 ) and Exoscale API environment (default api , can be used to target non-production API server) respectively.","title":"Deployment"},{"location":"tutorials/exoscale/#rbac","text":"If your cluster is RBAC enabled, you also need to setup the following, before you can run external-dns: apiVersion : v1 kind : ServiceAccount metadata : name : external-dns namespace : default --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default","title":"RBAC"},{"location":"tutorials/exoscale/#testing-and-verification","text":"Important! : Remember to change example.com with your own domain throughout the following text. Spin up a simple nginx HTTP server with the following spec ( kubectl apply -f ): apiVersion : networking.k8s.io/v1 kind : Ingress metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/target : {{ Elastic-IP-address }} spec : ingressClassName : nginx rules : - host : via-ingress.example.com http : paths : - backend : service : name : \"nginx\" port : number : 80 path : / pathType : Prefix --- apiVersion : v1 kind : Service metadata : name : nginx spec : ports : - port : 80 targetPort : 80 selector : app : nginx --- apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 Important! : Don\u2019t run dig, nslookup or similar immediately (until you\u2019ve confirmed the record exists). You\u2019ll get hit by negative DNS caching , which is hard to flush. Wait about 30s-1m (interval for external-dns to kick in), then check Exoscales portal \u2026 via-ingress.example.com should appear as a A and TXT record with your Elastic-IP-address.","title":"Testing and Verification"},{"location":"tutorials/externalname/","text":"Setting up ExternalDNS for ExternalName Services \u00b6 This tutorial describes how to setup ExternalDNS for usage in conjunction with an ExternalName service. Use cases \u00b6 The main use cases that inspired this feature is the necessity for having a subdomain pointing to an external domain. In this scenario, it makes sense for the subdomain to have a CNAME record pointing to the external domain. Setup \u00b6 External DNS \u00b6 apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --log-level=debug - --source=service - --source=ingress - --namespace=dev - --domain-filter=example.org. - --provider=aws - --registry=txt - --txt-owner-id=dev.example.org ExternalName Service \u00b6 kind : Service apiVersion : v1 metadata : name : aws-service annotations : external-dns.alpha.kubernetes.io/hostname : tenant1.example.org,tenant2.example.org spec : type : ExternalName externalName : aws.example.org This will create 2 CNAME records pointing to aws.example.org : tenant1.example.org tenant2.example.org ExternalName Service with an IP address \u00b6 If externalName is an IP address, External DNS will create A records instead of CNAME. kind : Service apiVersion : v1 metadata : name : aws-service annotations : external-dns.alpha.kubernetes.io/hostname : tenant1.example.org,tenant2.example.org spec : type : ExternalName externalName : 111.111.111.111 This will create 2 A records pointing to 111.111.111.111 : tenant1.example.org tenant2.example.org","title":"Setting up ExternalDNS for ExternalName Services"},{"location":"tutorials/externalname/#setting-up-externaldns-for-externalname-services","text":"This tutorial describes how to setup ExternalDNS for usage in conjunction with an ExternalName service.","title":"Setting up ExternalDNS for ExternalName Services"},{"location":"tutorials/externalname/#use-cases","text":"The main use cases that inspired this feature is the necessity for having a subdomain pointing to an external domain. In this scenario, it makes sense for the subdomain to have a CNAME record pointing to the external domain.","title":"Use cases"},{"location":"tutorials/externalname/#setup","text":"","title":"Setup"},{"location":"tutorials/externalname/#external-dns","text":"apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --log-level=debug - --source=service - --source=ingress - --namespace=dev - --domain-filter=example.org. - --provider=aws - --registry=txt - --txt-owner-id=dev.example.org","title":"External DNS"},{"location":"tutorials/externalname/#externalname-service","text":"kind : Service apiVersion : v1 metadata : name : aws-service annotations : external-dns.alpha.kubernetes.io/hostname : tenant1.example.org,tenant2.example.org spec : type : ExternalName externalName : aws.example.org This will create 2 CNAME records pointing to aws.example.org : tenant1.example.org tenant2.example.org","title":"ExternalName Service"},{"location":"tutorials/externalname/#externalname-service-with-an-ip-address","text":"If externalName is an IP address, External DNS will create A records instead of CNAME. kind : Service apiVersion : v1 metadata : name : aws-service annotations : external-dns.alpha.kubernetes.io/hostname : tenant1.example.org,tenant2.example.org spec : type : ExternalName externalName : 111.111.111.111 This will create 2 A records pointing to 111.111.111.111 : tenant1.example.org tenant2.example.org","title":"ExternalName Service with an IP address"},{"location":"tutorials/f5-virtualserver/","text":"Configuring ExternalDNS to use the F5 Networks VirtualServer Source \u00b6 This tutorial describes how to configure ExternalDNS to use the F5 Networks VirtualServer Source. It is meant to supplement the other provider-specific setup tutorials. The F5 Networks VirtualServer CRD is part of this project. See more in-depth info regarding the VirtualServer CRD here . Start with ExternalDNS with the F5 Networks VirtualServer source \u00b6 Make sure that you have the k8s-bigip-ctlr installed in your cluster. The needed CRDs are bundled within the controller. In your Helm values.yaml add: sources: - ... - f5-virtualserver - ... or add it in your Deployment if you aren\u2019t installing external-dns via Helm: args: - --source=f5-virtualserver Note that, in case you\u2019re not installing via Helm, you\u2019ll need the following in the ClusterRole bound to the service account of external-dns : - apiGroups: - cis.f5.com resources: - virtualservers verbs: - get - list - watch","title":"Configuring ExternalDNS to use the F5 Networks VirtualServer Source"},{"location":"tutorials/f5-virtualserver/#configuring-externaldns-to-use-the-f5-networks-virtualserver-source","text":"This tutorial describes how to configure ExternalDNS to use the F5 Networks VirtualServer Source. It is meant to supplement the other provider-specific setup tutorials. The F5 Networks VirtualServer CRD is part of this project. See more in-depth info regarding the VirtualServer CRD here .","title":"Configuring ExternalDNS to use the F5 Networks VirtualServer Source"},{"location":"tutorials/f5-virtualserver/#start-with-externaldns-with-the-f5-networks-virtualserver-source","text":"Make sure that you have the k8s-bigip-ctlr installed in your cluster. The needed CRDs are bundled within the controller. In your Helm values.yaml add: sources: - ... - f5-virtualserver - ... or add it in your Deployment if you aren\u2019t installing external-dns via Helm: args: - --source=f5-virtualserver Note that, in case you\u2019re not installing via Helm, you\u2019ll need the following in the ClusterRole bound to the service account of external-dns : - apiGroups: - cis.f5.com resources: - virtualservers verbs: - get - list - watch","title":"Start with ExternalDNS with the F5 Networks VirtualServer source"},{"location":"tutorials/gandi/","text":"Setting up ExternalDNS for Services on Gandi \u00b6 This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using Gandi. Make sure to use >=0.7.7 version of ExternalDNS for this tutorial. Creating a Gandi DNS zone (domain) \u00b6 Create a new DNS zone where you want to create your records in. Let\u2019s use example.com as an example here. Make sure the zone uses Creating Gandi API Key \u00b6 Generate an API key on your account (click on \u201cSecurity\u201d). The environment variable GANDI_KEY will be needed to run ExternalDNS with Gandi. Deploy ExternalDNS \u00b6 Connect your kubectl client to the cluster you want to test ExternalDNS with. Then apply one of the following manifests file to deploy ExternalDNS. Manifest (for clusters without RBAC enabled) \u00b6 apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : replicas : 1 selector : matchLabels : app : external-dns strategy : type : Recreate template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=gandi env : - name : GANDI_KEY value : \"YOUR_GANDI_API_KEY\" Manifest (for clusters with RBAC enabled) \u00b6 apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" , \"watch\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : replicas : 1 selector : matchLabels : app : external-dns strategy : type : Recreate template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=gandi env : - name : GANDI_KEY value : \"YOUR_GANDI_API_KEY\" Deploying an Nginx Service \u00b6 Create a service file called \u2018nginx.yaml\u2019 with the following contents: apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : replicas : 1 selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 --- apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : my-app.example.com spec : selector : app : nginx type : LoadBalancer ports : - protocol : TCP port : 80 targetPort : 80 Note the annotation on the service; use the same hostname as the Gandi Domain. Make sure that your Domain is configured to use Live-DNS. ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records. Create the deployment and service: $ kubectl create -f nginx.yaml Depending where you run your service it can take a little while for your cloud provider to create an external IP for the service. Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize the Gandi DNS records. Verifying Gandi DNS records \u00b6 Check your Gandi Dashboard to view the records for your Gandi DNS zone. Click on the zone for the one created above if a different domain was used. This should show the external IP address of the service as the A record for your domain. Cleanup \u00b6 Now that we have verified that ExternalDNS will automatically manage Gandi DNS records, we can delete the tutorial\u2019s example: $ kubectl delete service -f nginx.yaml $ kubectl delete service -f externaldns.yaml Additional options \u00b6 If you\u2019re using organizations to separate your domains, you can pass the organization\u2019s ID in an environment variable called GANDI_SHARING_ID to get access to it.","title":"Setting up ExternalDNS for Services on Gandi"},{"location":"tutorials/gandi/#setting-up-externaldns-for-services-on-gandi","text":"This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using Gandi. Make sure to use >=0.7.7 version of ExternalDNS for this tutorial.","title":"Setting up ExternalDNS for Services on Gandi"},{"location":"tutorials/gandi/#creating-a-gandi-dns-zone-domain","text":"Create a new DNS zone where you want to create your records in. Let\u2019s use example.com as an example here. Make sure the zone uses","title":"Creating a Gandi DNS zone (domain)"},{"location":"tutorials/gandi/#creating-gandi-api-key","text":"Generate an API key on your account (click on \u201cSecurity\u201d). The environment variable GANDI_KEY will be needed to run ExternalDNS with Gandi.","title":"Creating Gandi API Key"},{"location":"tutorials/gandi/#deploy-externaldns","text":"Connect your kubectl client to the cluster you want to test ExternalDNS with. Then apply one of the following manifests file to deploy ExternalDNS.","title":"Deploy ExternalDNS"},{"location":"tutorials/gandi/#manifest-for-clusters-without-rbac-enabled","text":"apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : replicas : 1 selector : matchLabels : app : external-dns strategy : type : Recreate template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=gandi env : - name : GANDI_KEY value : \"YOUR_GANDI_API_KEY\"","title":"Manifest (for clusters without RBAC enabled)"},{"location":"tutorials/gandi/#manifest-for-clusters-with-rbac-enabled","text":"apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" , \"watch\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : replicas : 1 selector : matchLabels : app : external-dns strategy : type : Recreate template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=gandi env : - name : GANDI_KEY value : \"YOUR_GANDI_API_KEY\"","title":"Manifest (for clusters with RBAC enabled)"},{"location":"tutorials/gandi/#deploying-an-nginx-service","text":"Create a service file called \u2018nginx.yaml\u2019 with the following contents: apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : replicas : 1 selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 --- apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : my-app.example.com spec : selector : app : nginx type : LoadBalancer ports : - protocol : TCP port : 80 targetPort : 80 Note the annotation on the service; use the same hostname as the Gandi Domain. Make sure that your Domain is configured to use Live-DNS. ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records. Create the deployment and service: $ kubectl create -f nginx.yaml Depending where you run your service it can take a little while for your cloud provider to create an external IP for the service. Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize the Gandi DNS records.","title":"Deploying an Nginx Service"},{"location":"tutorials/gandi/#verifying-gandi-dns-records","text":"Check your Gandi Dashboard to view the records for your Gandi DNS zone. Click on the zone for the one created above if a different domain was used. This should show the external IP address of the service as the A record for your domain.","title":"Verifying Gandi DNS records"},{"location":"tutorials/gandi/#cleanup","text":"Now that we have verified that ExternalDNS will automatically manage Gandi DNS records, we can delete the tutorial\u2019s example: $ kubectl delete service -f nginx.yaml $ kubectl delete service -f externaldns.yaml","title":"Cleanup"},{"location":"tutorials/gandi/#additional-options","text":"If you\u2019re using organizations to separate your domains, you can pass the organization\u2019s ID in an environment variable called GANDI_SHARING_ID to get access to it.","title":"Additional options"},{"location":"tutorials/gateway-api/","text":"Configuring ExternalDNS to use Gateway API Route Sources \u00b6 This describes how to configure ExternalDNS to use Gateway API Route sources. It is meant to supplement the other provider-specific setup tutorials. Supported API Versions \u00b6 As the Gateway API is still in an experimental phase, ExternalDNS makes no backwards compatibilty guarantees regarding its support. However, it currently supports a mixture of v1alpha2 and v1beta1 APIs. Gateways and HTTPRoutes are supported using the v1beta1 API. GRPCRoutes, TLSRoutes, TCPRoutes, and UDPRoutes are supported using the v1alpha2 API. Hostnames \u00b6 HTTPRoute and TLSRoute specs, along with their associated Gateway Listeners, contain hostnames that will be used by ExternalDNS. However, no such hostnames may be specified in TCPRoute or UDPRoute specs. For TCPRoutes and UDPRoutes, the external-dns.alpha.kubernetes.io/hostname annotation is the recommended way to provide their hostnames to ExternalDNS. This annotation is also supported for HTTPRoutes and TLSRoutes by ExternalDNS, but it\u2019s strongly recommended that they use their specs to provide all intended hostnames, since the Gateway that ultimately routes their requests/connections won\u2019t recognize additional hostnames from the annotation. Manifest with RBAC \u00b6 apiVersion : v1 kind : ServiceAccount metadata : name : external-dns namespace : default --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"namespaces\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"gateway.networking.k8s.io\" ] resources : [ \"gateways\" , \"httproutes\" , \"grpcroutes\" , \"tlsroutes\" , \"tcproutes\" , \"udproutes\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns namespace : default spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : # Add desired Gateway API Route sources. - --source=gateway-httproute - --source=gateway-grpcroute - --source=gateway-tlsroute - --source=gateway-tcproute - --source=gateway-udproute # Optionally, limit Routes to those in the given namespace. - --namespace=my-route-namespace # Optionally, limit Routes to those matching the given label selector. - --label-filter=my-route-label==my-route-value # Optionally, limit Route endpoints to those Gateways in the given namespace. - --gateway-namespace=my-gateway-namespace # Optionally, limit Route endpoints to those Gateways matching the given label selector. - --gateway-label-filter=my-gateway-label==my-gateway-value # Add provider-specific flags... - --domain-filter=external-dns-test.my-org.com - --provider=google - --registry=txt - --txt-owner-id=my-identifier","title":"Configuring ExternalDNS to use Gateway API Route Sources"},{"location":"tutorials/gateway-api/#configuring-externaldns-to-use-gateway-api-route-sources","text":"This describes how to configure ExternalDNS to use Gateway API Route sources. It is meant to supplement the other provider-specific setup tutorials.","title":"Configuring ExternalDNS to use Gateway API Route Sources"},{"location":"tutorials/gateway-api/#supported-api-versions","text":"As the Gateway API is still in an experimental phase, ExternalDNS makes no backwards compatibilty guarantees regarding its support. However, it currently supports a mixture of v1alpha2 and v1beta1 APIs. Gateways and HTTPRoutes are supported using the v1beta1 API. GRPCRoutes, TLSRoutes, TCPRoutes, and UDPRoutes are supported using the v1alpha2 API.","title":"Supported API Versions"},{"location":"tutorials/gateway-api/#hostnames","text":"HTTPRoute and TLSRoute specs, along with their associated Gateway Listeners, contain hostnames that will be used by ExternalDNS. However, no such hostnames may be specified in TCPRoute or UDPRoute specs. For TCPRoutes and UDPRoutes, the external-dns.alpha.kubernetes.io/hostname annotation is the recommended way to provide their hostnames to ExternalDNS. This annotation is also supported for HTTPRoutes and TLSRoutes by ExternalDNS, but it\u2019s strongly recommended that they use their specs to provide all intended hostnames, since the Gateway that ultimately routes their requests/connections won\u2019t recognize additional hostnames from the annotation.","title":"Hostnames"},{"location":"tutorials/gateway-api/#manifest-with-rbac","text":"apiVersion : v1 kind : ServiceAccount metadata : name : external-dns namespace : default --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"namespaces\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"gateway.networking.k8s.io\" ] resources : [ \"gateways\" , \"httproutes\" , \"grpcroutes\" , \"tlsroutes\" , \"tcproutes\" , \"udproutes\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns namespace : default spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : # Add desired Gateway API Route sources. - --source=gateway-httproute - --source=gateway-grpcroute - --source=gateway-tlsroute - --source=gateway-tcproute - --source=gateway-udproute # Optionally, limit Routes to those in the given namespace. - --namespace=my-route-namespace # Optionally, limit Routes to those matching the given label selector. - --label-filter=my-route-label==my-route-value # Optionally, limit Route endpoints to those Gateways in the given namespace. - --gateway-namespace=my-gateway-namespace # Optionally, limit Route endpoints to those Gateways matching the given label selector. - --gateway-label-filter=my-gateway-label==my-gateway-value # Add provider-specific flags... - --domain-filter=external-dns-test.my-org.com - --provider=google - --registry=txt - --txt-owner-id=my-identifier","title":"Manifest with RBAC"},{"location":"tutorials/gke/","text":"Setting up ExternalDNS on Google Kubernetes Engine \u00b6 This tutorial describes how to setup ExternalDNS for usage within a GKE ( Google Kuberentes Engine ) cluster. Make sure to use >=0.11.0 version of ExternalDNS for this tutorial Single project test scenario using access scopes \u00b6 If you prefer to try-out ExternalDNS in one of the existing environments you can skip this step The following instructions use access scopes to provide ExternalDNS with the permissions it needs to manage DNS records within a single project , the organizing entity to allocate resources. Note that since these permissions are associated with the instance, all pods in the cluster will also have these permissions. As such, this approach is not suitable for anything but testing environments. This solution will only work when both CloudDNS and GKE are provisioned in the same project. If the CloudDNS zone is in a different project, this solution will not work. Configure Project Environment \u00b6 Setup your environment to work with Google Cloud Platform. Fill in your variables as needed, e.g. target project. # set variables to the appropriate desired values PROJECT_ID = \"my-external-dns-test\" REGION = \"europe-west1\" ZONE = \"europe-west1-d\" ClOUD_BILLING_ACCOUNT = \"\" # set default settings for project gcloud config set project $PROJECT_ID gcloud config set compute/region $REGION gcloud config set compute/zone $ZONE # enable billing and APIs if not done already gcloud beta billing projects link $PROJECT_ID \\ --billing-account $BILLING_ACCOUNT gcloud services enable \"dns.googleapis.com\" gcloud services enable \"container.googleapis.com\" Create GKE Cluster \u00b6 gcloud container clusters create $GKE_CLUSTER_NAME \\ --num-nodes 1 \\ --scopes \"https://www.googleapis.com/auth/ndev.clouddns.readwrite\" WARNING : Note that this cluster will use the default compute engine GSA that contians the overly permissive project editor ( roles/editor ) role. So essentially, anything on the cluster could potentially grant escalated privileges. Also, as mentioned earlier, the access scope ndev.clouddns.readwrite will allow anything running on the cluster to have read/write permissions on all Cloud DNS zones within the same project. Cloud DNS Zone \u00b6 Create a DNS zone which will contain the managed DNS records. If using your own domain that was registered with a third-party domain registrar, you should point your domain\u2019s name servers to the values under the nameServers key. Please consult your registrar\u2019s documentation on how to do that. This tutorial will use example domain of example.com . gcloud dns managed-zones create \"example-com\" --dns-name \"example.com.\" \\ --description \"Automatically managed zone by kubernetes.io/external-dns\" Make a note of the nameservers that were assigned to your new zone. gcloud dns record-sets list \\ --zone \"example-com\" --name \"example.com.\" --type NS Outputs: NAME TYPE TTL DATA example.com. NS 21600 ns-cloud-e1.googledomains.com.,ns-cloud-e2.googledomains.com.,ns-cloud-e3.googledomains.com.,ns-cloud-e4.googledomains.com. In this case it\u2019s ns-cloud-{e1-e4}.googledomains.com. but your\u2019s could slightly differ, e.g. {a1-a4} , {b1-b4} etc. Cross project access scenario using Google Service Account \u00b6 More often, following best practices in regards to security and operations, Cloud DNS zones will be managed in a separate project from the Kubernetes cluster. This section shows how setup ExternalDNS to access Cloud DNS from a different project. These steps will also work for single project scenarios as well. ExternalDNS will need permissions to make changes to the Cloud DNS zone. There are three ways to configure the access needed: Worker Node Service Account Static Credentials Work Load Identity Setup Cloud DNS and GKE \u00b6 Below are examples on how you can configure Cloud DNS and GKE in separate projects, and then use one of the three methods to grant access to ExternalDNS. Replace the environment variables to values that make sense in your environment. Configure Projects \u00b6 For this process, create projects with the appropriate APIs enabled. # set variables to appropriate desired values GKE_PROJECT_ID = \"my-workload-project\" DNS_PROJECT_ID = \"my-cloud-dns-project\" ClOUD_BILLING_ACCOUNT = \"\" # enable billing and APIs for DNS project if not done already gcloud config set project $DNS_PROJECT_ID gcloud beta billing projects link $CLOUD_DNS_PROJECT \\ --billing-account $ClOUD_BILLING_ACCOUNT gcloud services enable \"dns.googleapis.com\" # enable billing and APIs for GKE project if not done already gcloud config set project $GKE_PROJECT_ID gcloud beta billing projects link $CLOUD_DNS_PROJECT \\ --billing-account $ClOUD_BILLING_ACCOUNT gcloud services enable \"container.googleapis.com\" Provisioning Cloud DNS \u00b6 Create a Cloud DNS zone in the designated DNS project. gcloud dns managed-zones create \"example-com\" --project $DNS_PROJECT_ID \\ --description \"example.com\" --dns-name = \"example.com.\" --visibility = public If using your own domain that was registered with a third-party domain registrar, you should point your domain\u2019s name servers to the values under the nameServers key. Please consult your registrar\u2019s documentation on how to do that. The example domain of example.com will be used for this tutorial. Provisioning a GKE cluster for cross project access \u00b6 Create a GSA (Google Service Account) and grant it the minimal set of privileges required for GKE nodes: GKE_CLUSTER_NAME = \"my-external-dns-cluster\" GKE_REGION = \"us-central1\" GKE_SA_NAME = \"worker-nodes-sa\" GKE_SA_EMAIL = \" $GKE_SA_NAME @ ${ GKE_PROJECT_ID } .iam.gserviceaccount.com\" ROLES =( roles/logging.logWriter roles/monitoring.metricWriter roles/monitoring.viewer roles/stackdriver.resourceMetadata.writer ) gcloud iam service-accounts create $GKE_SA_NAME \\ --display-name $GKE_SA_NAME --project $GKE_PROJECT_ID # assign google service account to roles in GKE project for ROLE in ${ ROLES [*] } ; do gcloud projects add-iam-policy-binding $GKE_PROJECT_ID \\ --member \"serviceAccount: $GKE_SA_EMAIL \" \\ --role $ROLE done Create a cluster using this service account and enable workload identity : gcloud container clusters create $GKE_CLUSTER_NAME \\ --project $GKE_PROJECT_ID --region $GKE_REGION --num-nodes 1 \\ --service-account \" $GKE_SA_EMAIL \" \\ --workload-pool \" $GKE_PROJECT_ID .svc.id.goog\" Worker Node Service Account method \u00b6 In this method, the GSA (Google Service Account) that is associated with GKE worker nodes will be configured to have access to Cloud DNS. WARNING : This will grant access to modify the Cloud DNS zone records for all containers running on cluster, not just ExternalDNS, so use this option with caution. This is not recommended for production environments. GKE_SA_EMAIL = \" $GKE_SA_NAME @ ${ GKE_PROJECT_ID } .iam.gserviceaccount.com\" # assign google service account to dns.admin role in the cloud dns project gcloud projects add-iam-policy-binding $DNS_PROJECT_ID \\ --member serviceAccount: $GKE_SA_EMAIL \\ --role roles/dns.admin After this, follow the steps in Deploy ExternalDNS . Make sure to set the --google-project flag to match the Cloud DNS project name. Static Credentials \u00b6 In this scenario, a new GSA (Google Service Account) is created that has access to the CloudDNS zone. The credentials for this GSA are saved and installed as a Kubernetes secret that will be used by ExternalDNS. This allows only containers that have access to the secret, such as ExternalDNS to update records on the Cloud DNS Zone. Create GSA for use with static credentials \u00b6 DNS_SA_NAME = \"external-dns-sa\" DNS_SA_EMAIL = \" $DNS_SA_NAME @ ${ GKE_PROJECT_ID } .iam.gserviceaccount.com\" # create GSA used to access the Cloud DNS zone gcloud iam service-accounts create $DNS_SA_NAME --display-name $DNS_SA_NAME # assign google service account to dns.admin role in cloud-dns project gcloud projects add-iam-policy-binding $DNS_PROJECT_ID \\ --member serviceAccount: $DNS_SA_EMAIL --role \"roles/dns.admin\" Create Kubernetes secret using static credentials \u00b6 Generate static credentials from the ExternalDNS GSA. # download static credentials gcloud iam service-accounts keys create /local/path/to/credentials.json \\ --iam-account $DNS_SA_EMAIL Create a Kubernetes secret with the credentials in the same namespace of ExternalDNS. kubectl create secret generic \"external-dns\" --namespace ${ EXTERNALDNS_NS :- \"default\" } \\ --from-file /local/path/to/credentials.json After this, follow the steps in Deploy ExternalDNS . Make sure to set the --google-project flag to match Cloud DNS project name. Make sure to uncomment out the section that mounts the secret to the ExternalDNS pods. Workload Identity \u00b6 Workload Identity allows workloads in your GKE cluster to impersonate GSA (Google Service Accounts) using KSA (Kubernetes Service Accounts) configured during deployemnt. These are the steps to use this feature with ExternalDNS. Create GSA for use with Workload Identity \u00b6 DNS_SA_NAME = \"external-dns-sa\" DNS_SA_EMAIL = \" $DNS_SA_NAME @ ${ GKE_PROJECT_ID } .iam.gserviceaccount.com\" gcloud iam service-accounts create $DNS_SA_NAME --display-name $DNS_SA_NAME gcloud projects add-iam-policy-binding $DNS_PROJECT_ID \\ --member serviceAccount: $DNS_SA_EMAIL --role \"roles/dns.admin\" Link KSA to GSA \u00b6 Add an IAM policy binding bewtween the workload identity GSA and ExternalDNS GSA. This will link the ExternalDNS KSA to ExternalDNS GSA. gcloud iam service-accounts add-iam-policy-binding $DNS_SA_EMAIL \\ --role \"roles/iam.workloadIdentityUser\" \\ --member \"serviceAccount: $GKE_PROJECT_ID .svc.id.goog[ ${ EXTERNALDNS_NS :- \"default\" } /external-dns]\" Deploy External DNS \u00b6 Deploy ExternalDNS with the following steps below, documented under Deploy ExternalDNS . Set the --google-project flag to the Cloud DNS project name. Link KSA to GSA in Kubernetes \u00b6 Add the proper workload identity annotation to the ExternalDNS KSA. kubectl annotate serviceaccount \"external-dns\" \\ --namespace ${ EXTERNALDNS_NS :- \"default\" } \\ \"iam.gke.io/gcp-service-account= $DNS_SA_EMAIL \" Update ExternalDNS pods \u00b6 Update the Pod spec to schedule the workloads on nodes that use Workload Identity and to use the annotated Kubernetes service account. kubectl patch deployment \"external-dns\" \\ --namespace ${ EXTERNALDNS_NS :- \"default\" } \\ --patch \\ '{\"spec\": {\"template\": {\"spec\": {\"nodeSelector\": {\"iam.gke.io/gke-metadata-server-enabled\": \"true\"}}}}}' After all of these steps you may see several messages with googleapi: Error 403: Forbidden, forbidden . After several minutes when the token is refreshed, these error messages will go away, and you should see info messages, such as: All records are already up to date . Deploy ExternalDNS \u00b6 Then apply the following manifests file to deploy ExternalDNS. apiVersion : v1 kind : ServiceAccount metadata : name : external-dns labels : app.kubernetes.io/name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns labels : app.kubernetes.io/name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" , \"nodes\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer labels : app.kubernetes.io/name : external-dns roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default # change if namespace is not 'default' --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns labels : app.kubernetes.io/name : external-dns spec : strategy : type : Recreate selector : matchLabels : app.kubernetes.io/name : external-dns template : metadata : labels : app.kubernetes.io/name : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service - --source=ingress - --domain-filter=example.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones - --provider=google - --log-format=json # google cloud logs parses severity of the \"text\" log format incorrectly # - --google-project=my-cloud-dns-project # Use this to specify a project different from the one external-dns is running inside - --google-zone-visibility=public # Use this to filter to only zones with this visibility. Set to either 'public' or 'private'. Omitting will match public and private zones - --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization - --registry=txt - --txt-owner-id=my-identifier # # uncomment below if static credentials are used # env: # - name: GOOGLE_APPLICATION_CREDENTIALS # value: /etc/secrets/service-account/credentials.json # volumeMounts: # - name: google-service-account # mountPath: /etc/secrets/service-account/ # volumes: # - name: google-service-account # secret: # secretName: external-dns Create the deployment for ExternalDNS: kubectl create --namespace \"default\" --filename externaldns.yaml Verify ExternalDNS works \u00b6 The following will deploy a small nginx server that will be used to demonstrate that ExternalDNS is working. Verify using an external load balancer \u00b6 Create the following sample application to test that ExternalDNS works. This example will provision a L4 load balancer. apiVersion : v1 kind : Service metadata : name : nginx annotations : # change nginx.example.com to match an appropriate value external-dns.alpha.kubernetes.io/hostname : nginx.example.com spec : type : LoadBalancer ports : - port : 80 targetPort : 80 selector : app : nginx --- apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 Create the deployment and service objects: kubectl create --namespace \"default\" --filename nginx.yaml After roughly two minutes check that a corresponding DNS record for your service was created. gcloud dns record-sets list --zone \"example-com\" --name \"nginx.example.com.\" Example output: NAME TYPE TTL DATA nginx.example.com. A 300 104.155.60.49 nginx.example.com. TXT 300 \"heritage=external-dns,external-dns/owner=my-identifier\" Note created TXT record alongside A record. TXT record signifies that the corresponding A record is managed by ExternalDNS. This makes ExternalDNS safe for running in environments where there are other records managed via other means. Let\u2019s check that we can resolve this DNS name. We\u2019ll ask the nameservers assigned to your zone first. dig +short @ns-cloud-e1.googledomains.com. nginx.example.com. 104 .155.60.49 Given you hooked up your DNS zone with its parent zone you can use curl to access your site. curl nginx.example.com Verify using an ingress \u00b6 Let\u2019s check that Ingress works as well. Create the following Ingress. apiVersion : networking.k8s.io/v1 kind : Ingress metadata : name : nginx spec : rules : - host : server.example.com http : paths : - path : / pathType : Prefix backend : service : name : nginx port : number : 80 Create the ingress objects with: kubectl create --namespace \"default\" --filename ingress.yaml Note that this will ingress object will use the default ingress controller that comes with GKE to create a L7 load balancer in addition to the L4 load balancer previously with the service object. To use only the L7 load balancer, update the service manafest to change the Service type to NodePort and remove the ExternalDNS annotation. After roughly two minutes check that a corresponding DNS record for your Ingress was created. gcloud dns record-sets list \\ --zone \"example-com\" \\ --name \"server.example.com.\" \\ Output: NAME TYPE TTL DATA server.example.com. A 300 130.211.46.224 server.example.com. TXT 300 \"heritage=external-dns,external-dns/owner=my-identifier\" Let\u2019s check that we can resolve this DNS name as well. dig +short @ns-cloud-e1.googledomains.com. server.example.com. 130 .211.46.224 Try with curl as well. curl server.example.com Clean up \u00b6 Make sure to delete all Service and Ingress objects before terminating the cluster so all load balancers get cleaned up correctly. kubectl delete service nginx kubectl delete ingress nginx Give ExternalDNS some time to clean up the DNS records for you. Then delete the managed zone and cluster. gcloud dns managed-zones delete \"example-com\" gcloud container clusters delete \"external-dns\"","title":"Setting up ExternalDNS on Google Kubernetes Engine"},{"location":"tutorials/gke/#setting-up-externaldns-on-google-kubernetes-engine","text":"This tutorial describes how to setup ExternalDNS for usage within a GKE ( Google Kuberentes Engine ) cluster. Make sure to use >=0.11.0 version of ExternalDNS for this tutorial","title":"Setting up ExternalDNS on Google Kubernetes Engine"},{"location":"tutorials/gke/#single-project-test-scenario-using-access-scopes","text":"If you prefer to try-out ExternalDNS in one of the existing environments you can skip this step The following instructions use access scopes to provide ExternalDNS with the permissions it needs to manage DNS records within a single project , the organizing entity to allocate resources. Note that since these permissions are associated with the instance, all pods in the cluster will also have these permissions. As such, this approach is not suitable for anything but testing environments. This solution will only work when both CloudDNS and GKE are provisioned in the same project. If the CloudDNS zone is in a different project, this solution will not work.","title":"Single project test scenario using access scopes"},{"location":"tutorials/gke/#configure-project-environment","text":"Setup your environment to work with Google Cloud Platform. Fill in your variables as needed, e.g. target project. # set variables to the appropriate desired values PROJECT_ID = \"my-external-dns-test\" REGION = \"europe-west1\" ZONE = \"europe-west1-d\" ClOUD_BILLING_ACCOUNT = \"\" # set default settings for project gcloud config set project $PROJECT_ID gcloud config set compute/region $REGION gcloud config set compute/zone $ZONE # enable billing and APIs if not done already gcloud beta billing projects link $PROJECT_ID \\ --billing-account $BILLING_ACCOUNT gcloud services enable \"dns.googleapis.com\" gcloud services enable \"container.googleapis.com\"","title":"Configure Project Environment"},{"location":"tutorials/gke/#create-gke-cluster","text":"gcloud container clusters create $GKE_CLUSTER_NAME \\ --num-nodes 1 \\ --scopes \"https://www.googleapis.com/auth/ndev.clouddns.readwrite\" WARNING : Note that this cluster will use the default compute engine GSA that contians the overly permissive project editor ( roles/editor ) role. So essentially, anything on the cluster could potentially grant escalated privileges. Also, as mentioned earlier, the access scope ndev.clouddns.readwrite will allow anything running on the cluster to have read/write permissions on all Cloud DNS zones within the same project.","title":"Create GKE Cluster"},{"location":"tutorials/gke/#cloud-dns-zone","text":"Create a DNS zone which will contain the managed DNS records. If using your own domain that was registered with a third-party domain registrar, you should point your domain\u2019s name servers to the values under the nameServers key. Please consult your registrar\u2019s documentation on how to do that. This tutorial will use example domain of example.com . gcloud dns managed-zones create \"example-com\" --dns-name \"example.com.\" \\ --description \"Automatically managed zone by kubernetes.io/external-dns\" Make a note of the nameservers that were assigned to your new zone. gcloud dns record-sets list \\ --zone \"example-com\" --name \"example.com.\" --type NS Outputs: NAME TYPE TTL DATA example.com. NS 21600 ns-cloud-e1.googledomains.com.,ns-cloud-e2.googledomains.com.,ns-cloud-e3.googledomains.com.,ns-cloud-e4.googledomains.com. In this case it\u2019s ns-cloud-{e1-e4}.googledomains.com. but your\u2019s could slightly differ, e.g. {a1-a4} , {b1-b4} etc.","title":"Cloud DNS Zone"},{"location":"tutorials/gke/#cross-project-access-scenario-using-google-service-account","text":"More often, following best practices in regards to security and operations, Cloud DNS zones will be managed in a separate project from the Kubernetes cluster. This section shows how setup ExternalDNS to access Cloud DNS from a different project. These steps will also work for single project scenarios as well. ExternalDNS will need permissions to make changes to the Cloud DNS zone. There are three ways to configure the access needed: Worker Node Service Account Static Credentials Work Load Identity","title":"Cross project access scenario using Google Service Account"},{"location":"tutorials/gke/#setup-cloud-dns-and-gke","text":"Below are examples on how you can configure Cloud DNS and GKE in separate projects, and then use one of the three methods to grant access to ExternalDNS. Replace the environment variables to values that make sense in your environment.","title":"Setup Cloud DNS and GKE"},{"location":"tutorials/gke/#configure-projects","text":"For this process, create projects with the appropriate APIs enabled. # set variables to appropriate desired values GKE_PROJECT_ID = \"my-workload-project\" DNS_PROJECT_ID = \"my-cloud-dns-project\" ClOUD_BILLING_ACCOUNT = \"\" # enable billing and APIs for DNS project if not done already gcloud config set project $DNS_PROJECT_ID gcloud beta billing projects link $CLOUD_DNS_PROJECT \\ --billing-account $ClOUD_BILLING_ACCOUNT gcloud services enable \"dns.googleapis.com\" # enable billing and APIs for GKE project if not done already gcloud config set project $GKE_PROJECT_ID gcloud beta billing projects link $CLOUD_DNS_PROJECT \\ --billing-account $ClOUD_BILLING_ACCOUNT gcloud services enable \"container.googleapis.com\"","title":"Configure Projects"},{"location":"tutorials/gke/#provisioning-cloud-dns","text":"Create a Cloud DNS zone in the designated DNS project. gcloud dns managed-zones create \"example-com\" --project $DNS_PROJECT_ID \\ --description \"example.com\" --dns-name = \"example.com.\" --visibility = public If using your own domain that was registered with a third-party domain registrar, you should point your domain\u2019s name servers to the values under the nameServers key. Please consult your registrar\u2019s documentation on how to do that. The example domain of example.com will be used for this tutorial.","title":"Provisioning Cloud DNS"},{"location":"tutorials/gke/#provisioning-a-gke-cluster-for-cross-project-access","text":"Create a GSA (Google Service Account) and grant it the minimal set of privileges required for GKE nodes: GKE_CLUSTER_NAME = \"my-external-dns-cluster\" GKE_REGION = \"us-central1\" GKE_SA_NAME = \"worker-nodes-sa\" GKE_SA_EMAIL = \" $GKE_SA_NAME @ ${ GKE_PROJECT_ID } .iam.gserviceaccount.com\" ROLES =( roles/logging.logWriter roles/monitoring.metricWriter roles/monitoring.viewer roles/stackdriver.resourceMetadata.writer ) gcloud iam service-accounts create $GKE_SA_NAME \\ --display-name $GKE_SA_NAME --project $GKE_PROJECT_ID # assign google service account to roles in GKE project for ROLE in ${ ROLES [*] } ; do gcloud projects add-iam-policy-binding $GKE_PROJECT_ID \\ --member \"serviceAccount: $GKE_SA_EMAIL \" \\ --role $ROLE done Create a cluster using this service account and enable workload identity : gcloud container clusters create $GKE_CLUSTER_NAME \\ --project $GKE_PROJECT_ID --region $GKE_REGION --num-nodes 1 \\ --service-account \" $GKE_SA_EMAIL \" \\ --workload-pool \" $GKE_PROJECT_ID .svc.id.goog\"","title":"Provisioning a GKE cluster for cross project access"},{"location":"tutorials/gke/#worker-node-service-account-method","text":"In this method, the GSA (Google Service Account) that is associated with GKE worker nodes will be configured to have access to Cloud DNS. WARNING : This will grant access to modify the Cloud DNS zone records for all containers running on cluster, not just ExternalDNS, so use this option with caution. This is not recommended for production environments. GKE_SA_EMAIL = \" $GKE_SA_NAME @ ${ GKE_PROJECT_ID } .iam.gserviceaccount.com\" # assign google service account to dns.admin role in the cloud dns project gcloud projects add-iam-policy-binding $DNS_PROJECT_ID \\ --member serviceAccount: $GKE_SA_EMAIL \\ --role roles/dns.admin After this, follow the steps in Deploy ExternalDNS . Make sure to set the --google-project flag to match the Cloud DNS project name.","title":"Worker Node Service Account method"},{"location":"tutorials/gke/#static-credentials","text":"In this scenario, a new GSA (Google Service Account) is created that has access to the CloudDNS zone. The credentials for this GSA are saved and installed as a Kubernetes secret that will be used by ExternalDNS. This allows only containers that have access to the secret, such as ExternalDNS to update records on the Cloud DNS Zone.","title":"Static Credentials"},{"location":"tutorials/gke/#create-gsa-for-use-with-static-credentials","text":"DNS_SA_NAME = \"external-dns-sa\" DNS_SA_EMAIL = \" $DNS_SA_NAME @ ${ GKE_PROJECT_ID } .iam.gserviceaccount.com\" # create GSA used to access the Cloud DNS zone gcloud iam service-accounts create $DNS_SA_NAME --display-name $DNS_SA_NAME # assign google service account to dns.admin role in cloud-dns project gcloud projects add-iam-policy-binding $DNS_PROJECT_ID \\ --member serviceAccount: $DNS_SA_EMAIL --role \"roles/dns.admin\"","title":"Create GSA for use with static credentials"},{"location":"tutorials/gke/#create-kubernetes-secret-using-static-credentials","text":"Generate static credentials from the ExternalDNS GSA. # download static credentials gcloud iam service-accounts keys create /local/path/to/credentials.json \\ --iam-account $DNS_SA_EMAIL Create a Kubernetes secret with the credentials in the same namespace of ExternalDNS. kubectl create secret generic \"external-dns\" --namespace ${ EXTERNALDNS_NS :- \"default\" } \\ --from-file /local/path/to/credentials.json After this, follow the steps in Deploy ExternalDNS . Make sure to set the --google-project flag to match Cloud DNS project name. Make sure to uncomment out the section that mounts the secret to the ExternalDNS pods.","title":"Create Kubernetes secret using static credentials"},{"location":"tutorials/gke/#workload-identity","text":"Workload Identity allows workloads in your GKE cluster to impersonate GSA (Google Service Accounts) using KSA (Kubernetes Service Accounts) configured during deployemnt. These are the steps to use this feature with ExternalDNS.","title":"Workload Identity"},{"location":"tutorials/gke/#create-gsa-for-use-with-workload-identity","text":"DNS_SA_NAME = \"external-dns-sa\" DNS_SA_EMAIL = \" $DNS_SA_NAME @ ${ GKE_PROJECT_ID } .iam.gserviceaccount.com\" gcloud iam service-accounts create $DNS_SA_NAME --display-name $DNS_SA_NAME gcloud projects add-iam-policy-binding $DNS_PROJECT_ID \\ --member serviceAccount: $DNS_SA_EMAIL --role \"roles/dns.admin\"","title":"Create GSA for use with Workload Identity"},{"location":"tutorials/gke/#link-ksa-to-gsa","text":"Add an IAM policy binding bewtween the workload identity GSA and ExternalDNS GSA. This will link the ExternalDNS KSA to ExternalDNS GSA. gcloud iam service-accounts add-iam-policy-binding $DNS_SA_EMAIL \\ --role \"roles/iam.workloadIdentityUser\" \\ --member \"serviceAccount: $GKE_PROJECT_ID .svc.id.goog[ ${ EXTERNALDNS_NS :- \"default\" } /external-dns]\"","title":"Link KSA to GSA"},{"location":"tutorials/gke/#deploy-external-dns","text":"Deploy ExternalDNS with the following steps below, documented under Deploy ExternalDNS . Set the --google-project flag to the Cloud DNS project name.","title":"Deploy External DNS"},{"location":"tutorials/gke/#link-ksa-to-gsa-in-kubernetes","text":"Add the proper workload identity annotation to the ExternalDNS KSA. kubectl annotate serviceaccount \"external-dns\" \\ --namespace ${ EXTERNALDNS_NS :- \"default\" } \\ \"iam.gke.io/gcp-service-account= $DNS_SA_EMAIL \"","title":"Link KSA to GSA in Kubernetes"},{"location":"tutorials/gke/#update-externaldns-pods","text":"Update the Pod spec to schedule the workloads on nodes that use Workload Identity and to use the annotated Kubernetes service account. kubectl patch deployment \"external-dns\" \\ --namespace ${ EXTERNALDNS_NS :- \"default\" } \\ --patch \\ '{\"spec\": {\"template\": {\"spec\": {\"nodeSelector\": {\"iam.gke.io/gke-metadata-server-enabled\": \"true\"}}}}}' After all of these steps you may see several messages with googleapi: Error 403: Forbidden, forbidden . After several minutes when the token is refreshed, these error messages will go away, and you should see info messages, such as: All records are already up to date .","title":"Update ExternalDNS pods"},{"location":"tutorials/gke/#deploy-externaldns","text":"Then apply the following manifests file to deploy ExternalDNS. apiVersion : v1 kind : ServiceAccount metadata : name : external-dns labels : app.kubernetes.io/name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns labels : app.kubernetes.io/name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" , \"nodes\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer labels : app.kubernetes.io/name : external-dns roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default # change if namespace is not 'default' --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns labels : app.kubernetes.io/name : external-dns spec : strategy : type : Recreate selector : matchLabels : app.kubernetes.io/name : external-dns template : metadata : labels : app.kubernetes.io/name : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service - --source=ingress - --domain-filter=example.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones - --provider=google - --log-format=json # google cloud logs parses severity of the \"text\" log format incorrectly # - --google-project=my-cloud-dns-project # Use this to specify a project different from the one external-dns is running inside - --google-zone-visibility=public # Use this to filter to only zones with this visibility. Set to either 'public' or 'private'. Omitting will match public and private zones - --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization - --registry=txt - --txt-owner-id=my-identifier # # uncomment below if static credentials are used # env: # - name: GOOGLE_APPLICATION_CREDENTIALS # value: /etc/secrets/service-account/credentials.json # volumeMounts: # - name: google-service-account # mountPath: /etc/secrets/service-account/ # volumes: # - name: google-service-account # secret: # secretName: external-dns Create the deployment for ExternalDNS: kubectl create --namespace \"default\" --filename externaldns.yaml","title":"Deploy ExternalDNS"},{"location":"tutorials/gke/#verify-externaldns-works","text":"The following will deploy a small nginx server that will be used to demonstrate that ExternalDNS is working.","title":"Verify ExternalDNS works"},{"location":"tutorials/gke/#verify-using-an-external-load-balancer","text":"Create the following sample application to test that ExternalDNS works. This example will provision a L4 load balancer. apiVersion : v1 kind : Service metadata : name : nginx annotations : # change nginx.example.com to match an appropriate value external-dns.alpha.kubernetes.io/hostname : nginx.example.com spec : type : LoadBalancer ports : - port : 80 targetPort : 80 selector : app : nginx --- apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 Create the deployment and service objects: kubectl create --namespace \"default\" --filename nginx.yaml After roughly two minutes check that a corresponding DNS record for your service was created. gcloud dns record-sets list --zone \"example-com\" --name \"nginx.example.com.\" Example output: NAME TYPE TTL DATA nginx.example.com. A 300 104.155.60.49 nginx.example.com. TXT 300 \"heritage=external-dns,external-dns/owner=my-identifier\" Note created TXT record alongside A record. TXT record signifies that the corresponding A record is managed by ExternalDNS. This makes ExternalDNS safe for running in environments where there are other records managed via other means. Let\u2019s check that we can resolve this DNS name. We\u2019ll ask the nameservers assigned to your zone first. dig +short @ns-cloud-e1.googledomains.com. nginx.example.com. 104 .155.60.49 Given you hooked up your DNS zone with its parent zone you can use curl to access your site. curl nginx.example.com","title":"Verify using an external load balancer"},{"location":"tutorials/gke/#verify-using-an-ingress","text":"Let\u2019s check that Ingress works as well. Create the following Ingress. apiVersion : networking.k8s.io/v1 kind : Ingress metadata : name : nginx spec : rules : - host : server.example.com http : paths : - path : / pathType : Prefix backend : service : name : nginx port : number : 80 Create the ingress objects with: kubectl create --namespace \"default\" --filename ingress.yaml Note that this will ingress object will use the default ingress controller that comes with GKE to create a L7 load balancer in addition to the L4 load balancer previously with the service object. To use only the L7 load balancer, update the service manafest to change the Service type to NodePort and remove the ExternalDNS annotation. After roughly two minutes check that a corresponding DNS record for your Ingress was created. gcloud dns record-sets list \\ --zone \"example-com\" \\ --name \"server.example.com.\" \\ Output: NAME TYPE TTL DATA server.example.com. A 300 130.211.46.224 server.example.com. TXT 300 \"heritage=external-dns,external-dns/owner=my-identifier\" Let\u2019s check that we can resolve this DNS name as well. dig +short @ns-cloud-e1.googledomains.com. server.example.com. 130 .211.46.224 Try with curl as well. curl server.example.com","title":"Verify using an ingress"},{"location":"tutorials/gke/#clean-up","text":"Make sure to delete all Service and Ingress objects before terminating the cluster so all load balancers get cleaned up correctly. kubectl delete service nginx kubectl delete ingress nginx Give ExternalDNS some time to clean up the DNS records for you. Then delete the managed zone and cluster. gcloud dns managed-zones delete \"example-com\" gcloud container clusters delete \"external-dns\"","title":"Clean up"},{"location":"tutorials/gloo-proxy/","text":"Configuring ExternalDNS to use the Gloo Proxy Source \u00b6 This tutorial describes how to configure ExternalDNS to use the Gloo Proxy source. It is meant to supplement the other provider-specific setup tutorials. Manifest (for clusters without RBAC enabled) \u00b6 apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns # update this to the desired external-dns version image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=gloo-proxy - --gloo-namespace=custom-gloo-system # gloo system namespace. Omit to use the default (gloo-system) - --provider=aws - --registry=txt - --txt-owner-id=my-identifier Manifest (for clusters with RBAC enabled) \u00b6 Could be change if you have mulitple sources apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" , \"watch\" ] - apiGroups : [ \"gloo.solo.io\" ] resources : [ \"proxies\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"gateway.solo.io\" ] resources : [ \"virtualservices\" ] verbs : [ \"get\" , \"list\" , \"watch\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns # update this to the desired external-dns version image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=gloo-proxy - --gloo-namespace=custom-gloo-system # gloo system namespace. Omit to use the default (gloo-system) - --provider=aws - --registry=txt - --txt-owner-id=my-identifier","title":"Configuring ExternalDNS to use the Gloo Proxy Source"},{"location":"tutorials/gloo-proxy/#configuring-externaldns-to-use-the-gloo-proxy-source","text":"This tutorial describes how to configure ExternalDNS to use the Gloo Proxy source. It is meant to supplement the other provider-specific setup tutorials.","title":"Configuring ExternalDNS to use the Gloo Proxy Source"},{"location":"tutorials/gloo-proxy/#manifest-for-clusters-without-rbac-enabled","text":"apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns # update this to the desired external-dns version image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=gloo-proxy - --gloo-namespace=custom-gloo-system # gloo system namespace. Omit to use the default (gloo-system) - --provider=aws - --registry=txt - --txt-owner-id=my-identifier","title":"Manifest (for clusters without RBAC enabled)"},{"location":"tutorials/gloo-proxy/#manifest-for-clusters-with-rbac-enabled","text":"Could be change if you have mulitple sources apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" , \"watch\" ] - apiGroups : [ \"gloo.solo.io\" ] resources : [ \"proxies\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"gateway.solo.io\" ] resources : [ \"virtualservices\" ] verbs : [ \"get\" , \"list\" , \"watch\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns # update this to the desired external-dns version image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=gloo-proxy - --gloo-namespace=custom-gloo-system # gloo system namespace. Omit to use the default (gloo-system) - --provider=aws - --registry=txt - --txt-owner-id=my-identifier","title":"Manifest (for clusters with RBAC enabled)"},{"location":"tutorials/godaddy/","text":"Setting up ExternalDNS for Services on GoDaddy \u00b6 This tutorial describes how to setup ExternalDNS for use within a Kubernetes cluster using GoDaddy DNS. Make sure to use >=0.6 version of ExternalDNS for this tutorial. Creating a zone with GoDaddy DNS \u00b6 If you are new to GoDaddy, we recommend you first read the following instructions for creating a zone. Creating a zone using the GoDaddy web console Creating a zone using the GoDaddy API Creating GoDaddy API key \u00b6 You first need to create an API Key. Using the GoDaddy documentation you will have your API key and API secret Deploy ExternalDNS \u00b6 Connect your kubectl client to the cluster with which you want to test ExternalDNS, and then apply one of the following manifest files for deployment: Manifest (for clusters without RBAC enabled) \u00b6 apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=godaddy - --txt-prefix=external-dns. # In case of multiple k8s cluster - --txt-owner-id=owner-id # In case of multiple k8s cluster - --godaddy-api-key= - --godaddy-api-secret= Manifest (for clusters with RBAC enabled) \u00b6 apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" , \"watch\" ] - apiGroups : [ \"\" ] resources : [ \"endpoints\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=godaddy - --txt-prefix=external-dns. # In case of multiple k8s cluster - --txt-owner-id=owner-id # In case of multiple k8s cluster - --godaddy-api-key= - --godaddy-api-secret= Deploying an Nginx Service \u00b6 Create a service file called \u2018nginx.yaml\u2019 with the following contents: apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 --- apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : example.com external-dns.alpha.kubernetes.io/ttl : \"120\" #optional spec : selector : app : nginx type : LoadBalancer ports : - protocol : TCP port : 80 targetPort : 80 A note about annotations Verify that the annotation on the service uses the same hostname as the GoDaddy DNS zone created above. The annotation may also be a subdomain of the DNS zone (e.g. \u2018www.example.com\u2019). The TTL annotation can be used to configure the TTL on DNS records managed by ExternalDNS and is optional. If this annotation is not set, the TTL on records managed by ExternalDNS will default to 10. ExternalDNS uses the hostname annotation to determine which services should be registered with DNS. Removing the hostname annotation will cause ExternalDNS to remove the corresponding DNS records. Create the deployment and service \u00b6 $ kubectl create -f nginx.yaml Depending on where you run your service, it may take some time for your cloud provider to create an external IP for the service. Once an external IP is assigned, ExternalDNS detects the new service IP address and synchronizes the GoDaddy DNS records. Verifying GoDaddy DNS records \u00b6 Use the GoDaddy web console or API to verify that the A record for your domain shows the external IP address of the services. Cleanup \u00b6 Once you successfully configure and verify record management via ExternalDNS, you can delete the tutorial\u2019s example: $ kubectl delete -f nginx.yaml $ kubectl delete -f externaldns.yaml","title":"Setting up ExternalDNS for Services on GoDaddy"},{"location":"tutorials/godaddy/#setting-up-externaldns-for-services-on-godaddy","text":"This tutorial describes how to setup ExternalDNS for use within a Kubernetes cluster using GoDaddy DNS. Make sure to use >=0.6 version of ExternalDNS for this tutorial.","title":"Setting up ExternalDNS for Services on GoDaddy"},{"location":"tutorials/godaddy/#creating-a-zone-with-godaddy-dns","text":"If you are new to GoDaddy, we recommend you first read the following instructions for creating a zone. Creating a zone using the GoDaddy web console Creating a zone using the GoDaddy API","title":"Creating a zone with GoDaddy DNS"},{"location":"tutorials/godaddy/#creating-godaddy-api-key","text":"You first need to create an API Key. Using the GoDaddy documentation you will have your API key and API secret","title":"Creating GoDaddy API key"},{"location":"tutorials/godaddy/#deploy-externaldns","text":"Connect your kubectl client to the cluster with which you want to test ExternalDNS, and then apply one of the following manifest files for deployment:","title":"Deploy ExternalDNS"},{"location":"tutorials/godaddy/#manifest-for-clusters-without-rbac-enabled","text":"apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=godaddy - --txt-prefix=external-dns. # In case of multiple k8s cluster - --txt-owner-id=owner-id # In case of multiple k8s cluster - --godaddy-api-key= - --godaddy-api-secret=","title":"Manifest (for clusters without RBAC enabled)"},{"location":"tutorials/godaddy/#manifest-for-clusters-with-rbac-enabled","text":"apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" , \"watch\" ] - apiGroups : [ \"\" ] resources : [ \"endpoints\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=godaddy - --txt-prefix=external-dns. # In case of multiple k8s cluster - --txt-owner-id=owner-id # In case of multiple k8s cluster - --godaddy-api-key= - --godaddy-api-secret=","title":"Manifest (for clusters with RBAC enabled)"},{"location":"tutorials/godaddy/#deploying-an-nginx-service","text":"Create a service file called \u2018nginx.yaml\u2019 with the following contents: apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 --- apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : example.com external-dns.alpha.kubernetes.io/ttl : \"120\" #optional spec : selector : app : nginx type : LoadBalancer ports : - protocol : TCP port : 80 targetPort : 80 A note about annotations Verify that the annotation on the service uses the same hostname as the GoDaddy DNS zone created above. The annotation may also be a subdomain of the DNS zone (e.g. \u2018www.example.com\u2019). The TTL annotation can be used to configure the TTL on DNS records managed by ExternalDNS and is optional. If this annotation is not set, the TTL on records managed by ExternalDNS will default to 10. ExternalDNS uses the hostname annotation to determine which services should be registered with DNS. Removing the hostname annotation will cause ExternalDNS to remove the corresponding DNS records.","title":"Deploying an Nginx Service"},{"location":"tutorials/godaddy/#create-the-deployment-and-service","text":"$ kubectl create -f nginx.yaml Depending on where you run your service, it may take some time for your cloud provider to create an external IP for the service. Once an external IP is assigned, ExternalDNS detects the new service IP address and synchronizes the GoDaddy DNS records.","title":"Create the deployment and service"},{"location":"tutorials/godaddy/#verifying-godaddy-dns-records","text":"Use the GoDaddy web console or API to verify that the A record for your domain shows the external IP address of the services.","title":"Verifying GoDaddy DNS records"},{"location":"tutorials/godaddy/#cleanup","text":"Once you successfully configure and verify record management via ExternalDNS, you can delete the tutorial\u2019s example: $ kubectl delete -f nginx.yaml $ kubectl delete -f externaldns.yaml","title":"Cleanup"},{"location":"tutorials/hostport/","text":"Setting up ExternalDNS for Headless Services \u00b6 This tutorial describes how to setup ExternalDNS for usage in conjunction with a Headless service. Use cases \u00b6 The main use cases that inspired this feature is the necessity for fixed addressable hostnames with services, such as Kafka when trying to access them from outside the cluster. In this scenario, quite often, only the Node IP addresses are actually routable and as in systems like Kafka more direct connections are preferable. Setup \u00b6 We will go through a small example of deploying a simple Kafka with use of a headless service. External DNS \u00b6 A simple deploy could look like this: Manifest (for clusters without RBAC enabled) \u00b6 apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --log-level=debug - --source=service - --source=ingress - --namespace=dev - --domain-filter=example.org. - --provider=aws - --registry=txt - --txt-owner-id=dev.example.org Manifest (for clusters with RBAC enabled) \u00b6 apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --log-level=debug - --source=service - --source=ingress - --namespace=dev - --domain-filter=example.org. - --provider=aws - --registry=txt - --txt-owner-id=dev.example.org Kafka Stateful Set \u00b6 First lets deploy a Kafka Stateful set, a simple example(a lot of stuff is missing) with a headless service called ksvc apiVersion : apps/v1 kind : StatefulSet metadata : name : kafka spec : serviceName : ksvc replicas : 3 template : metadata : labels : component : kafka spec : containers : - name : kafka image : confluent/kafka ports : - containerPort : 9092 hostPort : 9092 name : external command : - bash - -c - \" export DOMAIN=$(hostname -d) && \\ export KAFKA_BROKER_ID=$(echo $HOSTNAME|rev|cut -d '-' -f 1|rev) && \\ export KAFKA_ZOOKEEPER_CONNECT=$ZK_CSVC_SERVICE_HOST:$ZK_CSVC_SERVICE_PORT && \\ export KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://$HOSTNAME.example.org:9092 && \\ /etc/confluent/docker/run\" volumeMounts : - name : datadir mountPath : /var/lib/kafka volumeClaimTemplates : - metadata : name : datadir annotations : volume.beta.kubernetes.io/storage-class : st1 spec : accessModes : [ \"ReadWriteOnce\" ] resources : requests : storage : 500Gi Very important here, is to set the hostPort (only works if the PodSecurityPolicy allows it)! and in case your app requires an actual hostname inside the container, unlike Kafka, which can advertise on another address, you have to set the hostname yourself. Headless Service \u00b6 Now we need to define a headless service to use to expose the Kafka pods. There are generally two approaches to use expose the nodeport of a Headless service: Add --fqdn-template={{name}}.example.org Use a full annotation If you go with #1, you just need to define the headless service, here is an example of the case #2: apiVersion : v1 kind : Service metadata : name : ksvc annotations : external-dns.alpha.kubernetes.io/hostname : example.org spec : ports : - port : 9092 name : external clusterIP : None selector : component : kafka This will create 3 dns records: kafka-0.example.org kafka-1.example.org kafka-2.example.org If you set --fqdn-template={{name}}.example.org you can omit the annotation. Generally it is a better approach to use --fqdn-template={{name}}.example.org , because then you would get the service name inside the generated A records: kafka-0.ksvc.example.org kafka-1.ksvc.example.org kafka-2.ksvc.example.org Using pods\u2019 HostIPs as targets \u00b6 Add the following annotation to your Service : external-dns.alpha.kubernetes.io/endpoints-type : HostIP external-dns will now publish the value of the .status.hostIP field of the pods backing your Service . Using node external IPs as targets \u00b6 Add the following annotation to your Service : external-dns.alpha.kubernetes.io/endpoints-type : NodeExternalIP external-dns will now publish the node external IP ( .status.addresses entries of with type: NodeExternalIP ) of the nodes on which the pods backing your Service are running. Using pod annotations to specify target IPs \u00b6 Add the following annotation to the pods backing your Service : external-dns.alpha.kubernetes.io/target : \"1.2.3.4\" external-dns will publish the IP specified in the annotation of each pod instead of using the podIP advertised by Kubernetes. This can be useful e.g. if you are NATing public IPs onto your pod IPs and want to publish these in DNS.","title":"Setting up ExternalDNS for Headless Services"},{"location":"tutorials/hostport/#setting-up-externaldns-for-headless-services","text":"This tutorial describes how to setup ExternalDNS for usage in conjunction with a Headless service.","title":"Setting up ExternalDNS for Headless Services"},{"location":"tutorials/hostport/#use-cases","text":"The main use cases that inspired this feature is the necessity for fixed addressable hostnames with services, such as Kafka when trying to access them from outside the cluster. In this scenario, quite often, only the Node IP addresses are actually routable and as in systems like Kafka more direct connections are preferable.","title":"Use cases"},{"location":"tutorials/hostport/#setup","text":"We will go through a small example of deploying a simple Kafka with use of a headless service.","title":"Setup"},{"location":"tutorials/hostport/#external-dns","text":"A simple deploy could look like this:","title":"External DNS"},{"location":"tutorials/hostport/#manifest-for-clusters-without-rbac-enabled","text":"apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --log-level=debug - --source=service - --source=ingress - --namespace=dev - --domain-filter=example.org. - --provider=aws - --registry=txt - --txt-owner-id=dev.example.org","title":"Manifest (for clusters without RBAC enabled)"},{"location":"tutorials/hostport/#manifest-for-clusters-with-rbac-enabled","text":"apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --log-level=debug - --source=service - --source=ingress - --namespace=dev - --domain-filter=example.org. - --provider=aws - --registry=txt - --txt-owner-id=dev.example.org","title":"Manifest (for clusters with RBAC enabled)"},{"location":"tutorials/hostport/#kafka-stateful-set","text":"First lets deploy a Kafka Stateful set, a simple example(a lot of stuff is missing) with a headless service called ksvc apiVersion : apps/v1 kind : StatefulSet metadata : name : kafka spec : serviceName : ksvc replicas : 3 template : metadata : labels : component : kafka spec : containers : - name : kafka image : confluent/kafka ports : - containerPort : 9092 hostPort : 9092 name : external command : - bash - -c - \" export DOMAIN=$(hostname -d) && \\ export KAFKA_BROKER_ID=$(echo $HOSTNAME|rev|cut -d '-' -f 1|rev) && \\ export KAFKA_ZOOKEEPER_CONNECT=$ZK_CSVC_SERVICE_HOST:$ZK_CSVC_SERVICE_PORT && \\ export KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://$HOSTNAME.example.org:9092 && \\ /etc/confluent/docker/run\" volumeMounts : - name : datadir mountPath : /var/lib/kafka volumeClaimTemplates : - metadata : name : datadir annotations : volume.beta.kubernetes.io/storage-class : st1 spec : accessModes : [ \"ReadWriteOnce\" ] resources : requests : storage : 500Gi Very important here, is to set the hostPort (only works if the PodSecurityPolicy allows it)! and in case your app requires an actual hostname inside the container, unlike Kafka, which can advertise on another address, you have to set the hostname yourself.","title":"Kafka Stateful Set"},{"location":"tutorials/hostport/#headless-service","text":"Now we need to define a headless service to use to expose the Kafka pods. There are generally two approaches to use expose the nodeport of a Headless service: Add --fqdn-template={{name}}.example.org Use a full annotation If you go with #1, you just need to define the headless service, here is an example of the case #2: apiVersion : v1 kind : Service metadata : name : ksvc annotations : external-dns.alpha.kubernetes.io/hostname : example.org spec : ports : - port : 9092 name : external clusterIP : None selector : component : kafka This will create 3 dns records: kafka-0.example.org kafka-1.example.org kafka-2.example.org If you set --fqdn-template={{name}}.example.org you can omit the annotation. Generally it is a better approach to use --fqdn-template={{name}}.example.org , because then you would get the service name inside the generated A records: kafka-0.ksvc.example.org kafka-1.ksvc.example.org kafka-2.ksvc.example.org","title":"Headless Service"},{"location":"tutorials/hostport/#using-pods-hostips-as-targets","text":"Add the following annotation to your Service : external-dns.alpha.kubernetes.io/endpoints-type : HostIP external-dns will now publish the value of the .status.hostIP field of the pods backing your Service .","title":"Using pods' HostIPs as targets"},{"location":"tutorials/hostport/#using-node-external-ips-as-targets","text":"Add the following annotation to your Service : external-dns.alpha.kubernetes.io/endpoints-type : NodeExternalIP external-dns will now publish the node external IP ( .status.addresses entries of with type: NodeExternalIP ) of the nodes on which the pods backing your Service are running.","title":"Using node external IPs as targets"},{"location":"tutorials/hostport/#using-pod-annotations-to-specify-target-ips","text":"Add the following annotation to the pods backing your Service : external-dns.alpha.kubernetes.io/target : \"1.2.3.4\" external-dns will publish the IP specified in the annotation of each pod instead of using the podIP advertised by Kubernetes. This can be useful e.g. if you are NATing public IPs onto your pod IPs and want to publish these in DNS.","title":"Using pod annotations to specify target IPs"},{"location":"tutorials/ibmcloud/","text":"Setting up ExternalDNS for Services on IBMCloud \u00b6 This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using IBMCloud DNS. This tutorial uses IBMCloud CLI for all IBM Cloud commands and assumes that the Kubernetes cluster was created via IBM Cloud Kubernetes Service and kubectl commands are being run on an orchestration node. Creating a IBMCloud DNS zone \u00b6 The IBMCloud provider for ExternalDNS will find suitable zones for domains it manages; it will not automatically create zones. For public zone, This tutorial assume that the IBMCloud Internet Services was provisioned and the cis cli plugin was installed with IBMCloud CLI For private zone, This tutorial assume that the IBMCloud DNS Services was provisioned and the dns cli plugin was installed with IBMCloud CLI Public Zone \u00b6 For this tutorial, we create public zone named example.com on IBMCloud Internet Services instance external-dns-public $ ibmcloud cis domain-add example.com -i external-dns-public Follow step to active your zone Private Zone \u00b6 For this tutorial, we create private zone named example.com on IBMCloud DNS Services instance external-dns-private $ ibmcloud dns zone-create example.com -i external-dns-private Creating configuration file \u00b6 The preferred way to inject the configuration file is by using a Kubernetes secret. The secret should contain an object named azure.json with content similar to this: { \"apiKey\": \"1234567890abcdefghijklmnopqrstuvwxyz\", \"instanceCrn\": \"crn:v1:bluemix:public:internet-svcs:global:a/bcf1865e99742d38d2d5fc3fb80a5496:b950da8a-5be6-4691-810e-36388c77b0a3::\" } You can create or find the apiKey in your ibmcloud IAM \u2192 API Keys page You can find the instanceCrn in your service instance details Now you can create a file named \u2018ibmcloud.json\u2019 with values gathered above and with the structure of the example above. Use this file to create a Kubernetes secret: $ kubectl create secret generic ibmcloud-config-file --from-file=/local/path/to/ibmcloud.json Deploy ExternalDNS \u00b6 Connect your kubectl client to the cluster you want to test ExternalDNS with. Then apply one of the following manifests file to deploy ExternalDNS. Manifest (for clusters without RBAC enabled) \u00b6 apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=ibmcloud - --ibmcloud-proxied # (optional) enable the proxy feature of IBMCloud volumeMounts : - name : ibmcloud-config-file mountPath : /etc/kubernetes readOnly : true volumes : - name : ibmcloud-config-file secret : secretName : ibmcloud-config-file items : - key : externaldns-config.json path : ibmcloud.json Manifest (for clusters with RBAC enabled) \u00b6 apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" , \"watch\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=ibmcloud - --ibmcloud-proxied # (optional) enable the proxy feature of IBMCloud public zone volumeMounts : - name : ibmcloud-config-file mountPath : /etc/kubernetes readOnly : true volumes : - name : ibmcloud-config-file secret : secretName : ibmcloud-config-file items : - key : externaldns-config.json path : ibmcloud.json Deploying an Nginx Service \u00b6 Create a service file called nginx.yaml with the following contents: apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 --- apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : www.example.com external-dns.alpha.kubernetes.io/ttl : \"120\" #optional spec : selector : app : nginx type : LoadBalancer ports : - protocol : TCP port : 80 targetPort : 80 Note the annotation on the service; use the hostname as the IBMCloud DNS zone created above. The annotation may also be a subdomain of the DNS zone (e.g. \u2018www.example.com\u2019). By setting the TTL annotation on the service, you have to pass a valid TTL, which must be 120 or above. This annotation is optional, if you won\u2019t set it, it will be 1 (automatic) which is 300. ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records. Create the deployment and service: $ kubectl create -f nginx.yaml Depending where you run your service it can take a little while for your cloud provider to create an external IP for the service. Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize the IBMCloud DNS records. Verifying IBMCloud DNS records \u00b6 Run the following command to view the A records: Public Zone \u00b6 # Get the domain ID with below command on IBMCloud Internet Services instance `external-dns-public` $ ibmcloud cis domains -i external-dns-public # Get the records with domain ID $ ibmcloud cis dns-records DOMAIN_ID -i external-dns-public Private Zone \u00b6 # Get the domain ID with below command on IBMCloud DNS Services instance `external-dns-private` $ ibmcloud dns zones -i external-dns-private # Get the records with domain ID $ ibmcloud dns resource-records ZONE_ID -i external-dns-public This should show the external IP address of the service as the A record for your domain. Cleanup \u00b6 Now that we have verified that ExternalDNS will automatically manage IBMCloud DNS records, we can delete the tutorial\u2019s example: $ kubectl delete -f nginx.yaml $ kubectl delete -f externaldns.yaml Setting proxied records on public zone \u00b6 Using the external-dns.alpha.kubernetes.io/ibmcloud-proxied: \"true\" annotation on your ingress or service, you can specify if the proxy feature of IBMCloud public DNS should be enabled for that record. This setting will override the global --ibmcloud-proxied setting. Active priviate zone with VPC allocated \u00b6 By default, IBMCloud DNS Services don\u2019t active your private zone with new zone added, with externale DNS, you can use external-dns.alpha.kubernetes.io/ibmcloud-vpc: \"crn:v1:bluemix:public:is:us-south:a/bcf1865e99742d38d2d5fc3fb80a5496::vpc:r006-74353823-a60d-42e4-97c5-5e2551278435\" annotation on your ingress or service, it will active your private zone with in specific VPC for that record created in. this setting won\u2019t work if the private zone was active already. Note: the annotaion value is the VPC CRN, every IBM Cloud service have a valid CRN.","title":"Setting up ExternalDNS for Services on IBMCloud"},{"location":"tutorials/ibmcloud/#setting-up-externaldns-for-services-on-ibmcloud","text":"This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using IBMCloud DNS. This tutorial uses IBMCloud CLI for all IBM Cloud commands and assumes that the Kubernetes cluster was created via IBM Cloud Kubernetes Service and kubectl commands are being run on an orchestration node.","title":"Setting up ExternalDNS for Services on IBMCloud"},{"location":"tutorials/ibmcloud/#creating-a-ibmcloud-dns-zone","text":"The IBMCloud provider for ExternalDNS will find suitable zones for domains it manages; it will not automatically create zones. For public zone, This tutorial assume that the IBMCloud Internet Services was provisioned and the cis cli plugin was installed with IBMCloud CLI For private zone, This tutorial assume that the IBMCloud DNS Services was provisioned and the dns cli plugin was installed with IBMCloud CLI","title":"Creating a IBMCloud DNS zone"},{"location":"tutorials/ibmcloud/#public-zone","text":"For this tutorial, we create public zone named example.com on IBMCloud Internet Services instance external-dns-public $ ibmcloud cis domain-add example.com -i external-dns-public Follow step to active your zone","title":"Public Zone"},{"location":"tutorials/ibmcloud/#private-zone","text":"For this tutorial, we create private zone named example.com on IBMCloud DNS Services instance external-dns-private $ ibmcloud dns zone-create example.com -i external-dns-private","title":"Private Zone"},{"location":"tutorials/ibmcloud/#creating-configuration-file","text":"The preferred way to inject the configuration file is by using a Kubernetes secret. The secret should contain an object named azure.json with content similar to this: { \"apiKey\": \"1234567890abcdefghijklmnopqrstuvwxyz\", \"instanceCrn\": \"crn:v1:bluemix:public:internet-svcs:global:a/bcf1865e99742d38d2d5fc3fb80a5496:b950da8a-5be6-4691-810e-36388c77b0a3::\" } You can create or find the apiKey in your ibmcloud IAM \u2192 API Keys page You can find the instanceCrn in your service instance details Now you can create a file named \u2018ibmcloud.json\u2019 with values gathered above and with the structure of the example above. Use this file to create a Kubernetes secret: $ kubectl create secret generic ibmcloud-config-file --from-file=/local/path/to/ibmcloud.json","title":"Creating configuration file"},{"location":"tutorials/ibmcloud/#deploy-externaldns","text":"Connect your kubectl client to the cluster you want to test ExternalDNS with. Then apply one of the following manifests file to deploy ExternalDNS.","title":"Deploy ExternalDNS"},{"location":"tutorials/ibmcloud/#manifest-for-clusters-without-rbac-enabled","text":"apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=ibmcloud - --ibmcloud-proxied # (optional) enable the proxy feature of IBMCloud volumeMounts : - name : ibmcloud-config-file mountPath : /etc/kubernetes readOnly : true volumes : - name : ibmcloud-config-file secret : secretName : ibmcloud-config-file items : - key : externaldns-config.json path : ibmcloud.json","title":"Manifest (for clusters without RBAC enabled)"},{"location":"tutorials/ibmcloud/#manifest-for-clusters-with-rbac-enabled","text":"apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" , \"watch\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=ibmcloud - --ibmcloud-proxied # (optional) enable the proxy feature of IBMCloud public zone volumeMounts : - name : ibmcloud-config-file mountPath : /etc/kubernetes readOnly : true volumes : - name : ibmcloud-config-file secret : secretName : ibmcloud-config-file items : - key : externaldns-config.json path : ibmcloud.json","title":"Manifest (for clusters with RBAC enabled)"},{"location":"tutorials/ibmcloud/#deploying-an-nginx-service","text":"Create a service file called nginx.yaml with the following contents: apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 --- apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : www.example.com external-dns.alpha.kubernetes.io/ttl : \"120\" #optional spec : selector : app : nginx type : LoadBalancer ports : - protocol : TCP port : 80 targetPort : 80 Note the annotation on the service; use the hostname as the IBMCloud DNS zone created above. The annotation may also be a subdomain of the DNS zone (e.g. \u2018www.example.com\u2019). By setting the TTL annotation on the service, you have to pass a valid TTL, which must be 120 or above. This annotation is optional, if you won\u2019t set it, it will be 1 (automatic) which is 300. ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records. Create the deployment and service: $ kubectl create -f nginx.yaml Depending where you run your service it can take a little while for your cloud provider to create an external IP for the service. Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize the IBMCloud DNS records.","title":"Deploying an Nginx Service"},{"location":"tutorials/ibmcloud/#verifying-ibmcloud-dns-records","text":"Run the following command to view the A records:","title":"Verifying IBMCloud DNS records"},{"location":"tutorials/ibmcloud/#public-zone_1","text":"# Get the domain ID with below command on IBMCloud Internet Services instance `external-dns-public` $ ibmcloud cis domains -i external-dns-public # Get the records with domain ID $ ibmcloud cis dns-records DOMAIN_ID -i external-dns-public","title":"Public Zone"},{"location":"tutorials/ibmcloud/#private-zone_1","text":"# Get the domain ID with below command on IBMCloud DNS Services instance `external-dns-private` $ ibmcloud dns zones -i external-dns-private # Get the records with domain ID $ ibmcloud dns resource-records ZONE_ID -i external-dns-public This should show the external IP address of the service as the A record for your domain.","title":"Private Zone"},{"location":"tutorials/ibmcloud/#cleanup","text":"Now that we have verified that ExternalDNS will automatically manage IBMCloud DNS records, we can delete the tutorial\u2019s example: $ kubectl delete -f nginx.yaml $ kubectl delete -f externaldns.yaml","title":"Cleanup"},{"location":"tutorials/ibmcloud/#setting-proxied-records-on-public-zone","text":"Using the external-dns.alpha.kubernetes.io/ibmcloud-proxied: \"true\" annotation on your ingress or service, you can specify if the proxy feature of IBMCloud public DNS should be enabled for that record. This setting will override the global --ibmcloud-proxied setting.","title":"Setting proxied records on public zone"},{"location":"tutorials/ibmcloud/#active-priviate-zone-with-vpc-allocated","text":"By default, IBMCloud DNS Services don\u2019t active your private zone with new zone added, with externale DNS, you can use external-dns.alpha.kubernetes.io/ibmcloud-vpc: \"crn:v1:bluemix:public:is:us-south:a/bcf1865e99742d38d2d5fc3fb80a5496::vpc:r006-74353823-a60d-42e4-97c5-5e2551278435\" annotation on your ingress or service, it will active your private zone with in specific VPC for that record created in. this setting won\u2019t work if the private zone was active already. Note: the annotaion value is the VPC CRN, every IBM Cloud service have a valid CRN.","title":"Active priviate zone with VPC allocated"},{"location":"tutorials/infoblox/","text":"Setting up ExternalDNS for Infoblox \u00b6 This tutorial describes how to setup ExternalDNS for usage with Infoblox. Make sure to use >=0.4.6 version of ExternalDNS for this tutorial. The only WAPI version that has been validated is v2.3.1 . It is assumed that the API user has rights to create objects of the following types: zone_auth , record:a , record:cname , record:txt . This tutorial assumes you have substituted the correct values for the following environment variables: export GRID_HOST=127.0.0.1 export WAPI_PORT=443 export WAPI_VERSION=2.3.1 export WAPI_USERNAME=admin export WAPI_PASSWORD=infoblox Creating an Infoblox DNS zone \u00b6 The Infoblox provider for ExternalDNS will find suitable zones for domains it manages; it will not automatically create zones. Create an Infoblox DNS zone for \u201cexample.com\u201d: $ curl -kl \\ -X POST \\ -d fqdn=example.com \\ -u ${WAPI_USERNAME}:${WAPI_PASSWORD} \\ https://${GRID_HOST}:${WAPI_PORT}/wapi/v${WAPI_VERSION}/zone_auth Substitute a domain you own for \u201cexample.com\u201d if desired. Creating an Infoblox Configuration Secret \u00b6 For ExternalDNS to access the Infoblox API, create a Kubernetes secret. To create the secret: $ kubectl create secret generic external-dns \\ --from-literal=EXTERNAL_DNS_INFOBLOX_WAPI_USERNAME=${WAPI_USERNAME} \\ --from-literal=EXTERNAL_DNS_INFOBLOX_WAPI_PASSWORD=${WAPI_PASSWORD} Deploy ExternalDNS \u00b6 Connect your kubectl client to the cluster you want to test ExternalDNS with. Then apply one of the following manifests file to deploy ExternalDNS. Manifest (for clusters without RBAC enabled) \u00b6 apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service - --domain-filter=example.com # (optional) limit to only example.com domains. - --provider=infoblox - --infoblox-grid-host=${GRID_HOST} # (required) IP of the Infoblox Grid host. - --infoblox-wapi-port=443 # (optional) Infoblox WAPI port. The default is \"443\". - --infoblox-wapi-version=2.3.1 # (optional) Infoblox WAPI version. The default is \"2.3.1\" - --infoblox-ssl-verify # (optional) Use --no-infoblox-ssl-verify to skip server certificate verification. - --infoblox-create-ptr # (optional) Use --infoblox-create-ptr to create a ptr entry in addition to an entry. env : - name : EXTERNAL_DNS_INFOBLOX_HTTP_POOL_CONNECTIONS value : \"10\" # (optional) Infoblox WAPI request connection pool size. The default is \"10\". - name : EXTERNAL_DNS_INFOBLOX_HTTP_REQUEST_TIMEOUT value : \"60\" # (optional) Infoblox WAPI request timeout in seconds. The default is \"60\". - name : EXTERNAL_DNS_INFOBLOX_WAPI_USERNAME valueFrom : secretKeyRef : name : external-dns key : EXTERNAL_DNS_INFOBLOX_WAPI_USERNAME - name : EXTERNAL_DNS_INFOBLOX_WAPI_PASSWORD valueFrom : secretKeyRef : name : external-dns key : EXTERNAL_DNS_INFOBLOX_WAPI_PASSWORD Manifest (for clusters with RBAC enabled) \u00b6 apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service - --domain-filter=example.com # (optional) limit to only example.com domains. - --provider=infoblox - --infoblox-grid-host=${GRID_HOST} # (required) IP of the Infoblox Grid host. - --infoblox-wapi-port=443 # (optional) Infoblox WAPI port. The default is \"443\". - --infoblox-wapi-version=2.3.1 # (optional) Infoblox WAPI version. The default is \"2.3.1\" - --infoblox-ssl-verify # (optional) Use --no-infoblox-ssl-verify to skip server certificate verification. - --infoblox-create-ptr # (optional) Use --infoblox-create-ptr to create a ptr entry in addition to an entry. env : - name : EXTERNAL_DNS_INFOBLOX_HTTP_POOL_CONNECTIONS value : \"10\" # (optional) Infoblox WAPI request connection pool size. The default is \"10\". - name : EXTERNAL_DNS_INFOBLOX_HTTP_REQUEST_TIMEOUT value : \"60\" # (optional) Infoblox WAPI request timeout in seconds. The default is \"60\". - name : EXTERNAL_DNS_INFOBLOX_WAPI_USERNAME valueFrom : secretKeyRef : name : external-dns key : EXTERNAL_DNS_INFOBLOX_WAPI_USERNAME - name : EXTERNAL_DNS_INFOBLOX_WAPI_PASSWORD valueFrom : secretKeyRef : name : external-dns key : EXTERNAL_DNS_INFOBLOX_WAPI_PASSWORD Deploying an Nginx Service \u00b6 Create a service file called \u2018nginx.yaml\u2019 with the following contents: apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 --- apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : example.com spec : selector : app : nginx type : LoadBalancer ports : - protocol : TCP port : 80 targetPort : 80 Note the annotation on the service; use the same hostname as the Infoblox DNS zone created above. The annotation may also be a subdomain of the DNS zone (e.g. \u2018www.example.com\u2019). ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records. Create the deployment and service: $ kubectl create -f nginx.yaml It takes a little while for the Infoblox cloud provider to create an external IP for the service. Check the status by running kubectl get services nginx . If the EXTERNAL-IP field shows an address, the service is ready to be accessed externally. Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize the Infoblox DNS records. Verifying Infoblox DNS records \u00b6 Run the following command to view the A records for your Infoblox DNS zone: $ curl -kl \\ -X GET \\ -u ${WAPI_USERNAME}:${WAPI_PASSWORD} \\ https://${GRID_HOST}:${WAPI_PORT}/wapi/v${WAPI_VERSION}/record:a?zone=example.com Substitute the zone for the one created above if a different domain was used. This should show the external IP address of the service as the A record for your domain (\u2018@\u2019 indicates the record is for the zone itself). Clean up \u00b6 Now that we have verified that ExternalDNS will automatically manage Infoblox DNS records, we can delete the tutorial\u2019s DNS zone: $ curl -kl \\ -X DELETE \\ -u ${WAPI_USERNAME}:${WAPI_PASSWORD} \\ https://${GRID_HOST}:${WAPI_PORT}/wapi/v${WAPI_VERSION}/zone_auth?fqdn=example.com Ability to filter results from the zone auth API using a regular expression \u00b6 There is also the ability to filter results from the Infoblox zone_auth service based upon a regular expression. See the Infoblox API document for examples. To use this feature for the zone_auth service, set the parameter infoblox-fqdn-regex for external-dns to a regular expression that makes sense for you. For instance, to only return hosted zones that start with staging in the test.com domain (like staging.beta.test.com, or staging.test.com), use the following command line option when starting external-dns --infoblox-fqdn-regex=^staging.*test.com$ Ability to filter A, Host, CNAME and TXT records from the by name using a regular expression \u00b6 Infoblox supports filtering records by name using a regular expression. See the Infoblox API document for examples. To use this feature, set the parameter infoblox-name-regex for external-dns to a regular expression that makes sense for you. For instance, if all your dns records end with cluster1.example.com , you can fetch records matching this style by setting the following: --infoblox-name-regex=cluster1.example.com Infoblox PTR record support \u00b6 There is an option to enable PTR records support for infoblox provider. PTR records allow to do reverse dns search. To enable PTR records support, add following into arguments for external-dns: --infoblox-create-ptr to allow management of PTR records. You can also add a filter for reverse dns zone to limit PTR records to specific zones only: --domain-filter=10.196.0.0/16 change this to the reverse zone(s) as defined in your infoblox. Now external-dns will manage PTR records for you.","title":"Setting up ExternalDNS for Infoblox"},{"location":"tutorials/infoblox/#setting-up-externaldns-for-infoblox","text":"This tutorial describes how to setup ExternalDNS for usage with Infoblox. Make sure to use >=0.4.6 version of ExternalDNS for this tutorial. The only WAPI version that has been validated is v2.3.1 . It is assumed that the API user has rights to create objects of the following types: zone_auth , record:a , record:cname , record:txt . This tutorial assumes you have substituted the correct values for the following environment variables: export GRID_HOST=127.0.0.1 export WAPI_PORT=443 export WAPI_VERSION=2.3.1 export WAPI_USERNAME=admin export WAPI_PASSWORD=infoblox","title":"Setting up ExternalDNS for Infoblox"},{"location":"tutorials/infoblox/#creating-an-infoblox-dns-zone","text":"The Infoblox provider for ExternalDNS will find suitable zones for domains it manages; it will not automatically create zones. Create an Infoblox DNS zone for \u201cexample.com\u201d: $ curl -kl \\ -X POST \\ -d fqdn=example.com \\ -u ${WAPI_USERNAME}:${WAPI_PASSWORD} \\ https://${GRID_HOST}:${WAPI_PORT}/wapi/v${WAPI_VERSION}/zone_auth Substitute a domain you own for \u201cexample.com\u201d if desired.","title":"Creating an Infoblox DNS zone"},{"location":"tutorials/infoblox/#creating-an-infoblox-configuration-secret","text":"For ExternalDNS to access the Infoblox API, create a Kubernetes secret. To create the secret: $ kubectl create secret generic external-dns \\ --from-literal=EXTERNAL_DNS_INFOBLOX_WAPI_USERNAME=${WAPI_USERNAME} \\ --from-literal=EXTERNAL_DNS_INFOBLOX_WAPI_PASSWORD=${WAPI_PASSWORD}","title":"Creating an Infoblox Configuration Secret"},{"location":"tutorials/infoblox/#deploy-externaldns","text":"Connect your kubectl client to the cluster you want to test ExternalDNS with. Then apply one of the following manifests file to deploy ExternalDNS.","title":"Deploy ExternalDNS"},{"location":"tutorials/infoblox/#manifest-for-clusters-without-rbac-enabled","text":"apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service - --domain-filter=example.com # (optional) limit to only example.com domains. - --provider=infoblox - --infoblox-grid-host=${GRID_HOST} # (required) IP of the Infoblox Grid host. - --infoblox-wapi-port=443 # (optional) Infoblox WAPI port. The default is \"443\". - --infoblox-wapi-version=2.3.1 # (optional) Infoblox WAPI version. The default is \"2.3.1\" - --infoblox-ssl-verify # (optional) Use --no-infoblox-ssl-verify to skip server certificate verification. - --infoblox-create-ptr # (optional) Use --infoblox-create-ptr to create a ptr entry in addition to an entry. env : - name : EXTERNAL_DNS_INFOBLOX_HTTP_POOL_CONNECTIONS value : \"10\" # (optional) Infoblox WAPI request connection pool size. The default is \"10\". - name : EXTERNAL_DNS_INFOBLOX_HTTP_REQUEST_TIMEOUT value : \"60\" # (optional) Infoblox WAPI request timeout in seconds. The default is \"60\". - name : EXTERNAL_DNS_INFOBLOX_WAPI_USERNAME valueFrom : secretKeyRef : name : external-dns key : EXTERNAL_DNS_INFOBLOX_WAPI_USERNAME - name : EXTERNAL_DNS_INFOBLOX_WAPI_PASSWORD valueFrom : secretKeyRef : name : external-dns key : EXTERNAL_DNS_INFOBLOX_WAPI_PASSWORD","title":"Manifest (for clusters without RBAC enabled)"},{"location":"tutorials/infoblox/#manifest-for-clusters-with-rbac-enabled","text":"apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service - --domain-filter=example.com # (optional) limit to only example.com domains. - --provider=infoblox - --infoblox-grid-host=${GRID_HOST} # (required) IP of the Infoblox Grid host. - --infoblox-wapi-port=443 # (optional) Infoblox WAPI port. The default is \"443\". - --infoblox-wapi-version=2.3.1 # (optional) Infoblox WAPI version. The default is \"2.3.1\" - --infoblox-ssl-verify # (optional) Use --no-infoblox-ssl-verify to skip server certificate verification. - --infoblox-create-ptr # (optional) Use --infoblox-create-ptr to create a ptr entry in addition to an entry. env : - name : EXTERNAL_DNS_INFOBLOX_HTTP_POOL_CONNECTIONS value : \"10\" # (optional) Infoblox WAPI request connection pool size. The default is \"10\". - name : EXTERNAL_DNS_INFOBLOX_HTTP_REQUEST_TIMEOUT value : \"60\" # (optional) Infoblox WAPI request timeout in seconds. The default is \"60\". - name : EXTERNAL_DNS_INFOBLOX_WAPI_USERNAME valueFrom : secretKeyRef : name : external-dns key : EXTERNAL_DNS_INFOBLOX_WAPI_USERNAME - name : EXTERNAL_DNS_INFOBLOX_WAPI_PASSWORD valueFrom : secretKeyRef : name : external-dns key : EXTERNAL_DNS_INFOBLOX_WAPI_PASSWORD","title":"Manifest (for clusters with RBAC enabled)"},{"location":"tutorials/infoblox/#deploying-an-nginx-service","text":"Create a service file called \u2018nginx.yaml\u2019 with the following contents: apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 --- apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : example.com spec : selector : app : nginx type : LoadBalancer ports : - protocol : TCP port : 80 targetPort : 80 Note the annotation on the service; use the same hostname as the Infoblox DNS zone created above. The annotation may also be a subdomain of the DNS zone (e.g. \u2018www.example.com\u2019). ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records. Create the deployment and service: $ kubectl create -f nginx.yaml It takes a little while for the Infoblox cloud provider to create an external IP for the service. Check the status by running kubectl get services nginx . If the EXTERNAL-IP field shows an address, the service is ready to be accessed externally. Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize the Infoblox DNS records.","title":"Deploying an Nginx Service"},{"location":"tutorials/infoblox/#verifying-infoblox-dns-records","text":"Run the following command to view the A records for your Infoblox DNS zone: $ curl -kl \\ -X GET \\ -u ${WAPI_USERNAME}:${WAPI_PASSWORD} \\ https://${GRID_HOST}:${WAPI_PORT}/wapi/v${WAPI_VERSION}/record:a?zone=example.com Substitute the zone for the one created above if a different domain was used. This should show the external IP address of the service as the A record for your domain (\u2018@\u2019 indicates the record is for the zone itself).","title":"Verifying Infoblox DNS records"},{"location":"tutorials/infoblox/#clean-up","text":"Now that we have verified that ExternalDNS will automatically manage Infoblox DNS records, we can delete the tutorial\u2019s DNS zone: $ curl -kl \\ -X DELETE \\ -u ${WAPI_USERNAME}:${WAPI_PASSWORD} \\ https://${GRID_HOST}:${WAPI_PORT}/wapi/v${WAPI_VERSION}/zone_auth?fqdn=example.com","title":"Clean up"},{"location":"tutorials/infoblox/#ability-to-filter-results-from-the-zone-auth-api-using-a-regular-expression","text":"There is also the ability to filter results from the Infoblox zone_auth service based upon a regular expression. See the Infoblox API document for examples. To use this feature for the zone_auth service, set the parameter infoblox-fqdn-regex for external-dns to a regular expression that makes sense for you. For instance, to only return hosted zones that start with staging in the test.com domain (like staging.beta.test.com, or staging.test.com), use the following command line option when starting external-dns --infoblox-fqdn-regex=^staging.*test.com$","title":"Ability to filter results from the zone auth API using a regular expression"},{"location":"tutorials/infoblox/#ability-to-filter-a-host-cname-and-txt-records-from-the-by-name-using-a-regular-expression","text":"Infoblox supports filtering records by name using a regular expression. See the Infoblox API document for examples. To use this feature, set the parameter infoblox-name-regex for external-dns to a regular expression that makes sense for you. For instance, if all your dns records end with cluster1.example.com , you can fetch records matching this style by setting the following: --infoblox-name-regex=cluster1.example.com","title":"Ability to filter A, Host, CNAME and TXT records from the by name using a regular expression"},{"location":"tutorials/infoblox/#infoblox-ptr-record-support","text":"There is an option to enable PTR records support for infoblox provider. PTR records allow to do reverse dns search. To enable PTR records support, add following into arguments for external-dns: --infoblox-create-ptr to allow management of PTR records. You can also add a filter for reverse dns zone to limit PTR records to specific zones only: --domain-filter=10.196.0.0/16 change this to the reverse zone(s) as defined in your infoblox. Now external-dns will manage PTR records for you.","title":"Infoblox PTR record support"},{"location":"tutorials/istio/","text":"Configuring ExternalDNS to use the Istio Gateway and/or Istio Virtual Service Source \u00b6 This tutorial describes how to configure ExternalDNS to use the Istio Gateway source. It is meant to supplement the other provider-specific setup tutorials. Note: Using the Istio Gateway source requires Istio >=1.0.0. Manifest (for clusters without RBAC enabled) Manifest (for clusters with RBAC enabled) Update existing ExternalDNS Deployment Manifest (for clusters without RBAC enabled) \u00b6 apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service - --source=ingress - --source=istio-gateway # choose one - --source=istio-virtualservice # or both - --domain-filter=external-dns-test.my-org.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones - --provider=aws - --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization - --aws-zone-type=public # only look at public hosted zones (valid values are public, private or no value for both) - --registry=txt - --txt-owner-id=my-identifier Manifest (for clusters with RBAC enabled) \u00b6 apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" ] - apiGroups : [ \"networking.istio.io\" ] resources : [ \"gateways\" , \"virtualservices\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service - --source=ingress - --source=istio-gateway - --source=istio-virtualservice - --domain-filter=external-dns-test.my-org.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones - --provider=aws - --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization - --aws-zone-type=public # only look at public hosted zones (valid values are public, private or no value for both) - --registry=txt - --txt-owner-id=my-identifier Update existing ExternalDNS Deployment \u00b6 For clusters with running external-dns , you can just update the deployment. With access to the kube-system namespace, update the existing external-dns deployment. Add a parameter to the arguments of the container to create dns entries with --source=istio-gateway . Execute the following command or update the argument. kubectl patch deployment external-dns --type='json' \\ -p='[{\"op\": \"add\", \"path\": \"/spec/template/spec/containers/0/args/2\", \"value\": \"--source=istio-gateway\" }]' In case the setup uses a clusterrole , just append a new value to the enable the istio group. kubectl patch clusterrole external-dns --type='json' \\ -p='[{\"op\": \"add\", \"path\": \"/rules/4\", \"value\": { \"apiGroups\": [ \"networking.istio.io\"], \"resources\": [\"gateways\"],\"verbs\": [\"get\", \"watch\", \"list\" ]} }]' Verify that Istio Gateway/VirtualService Source works \u00b6 Follow the Istio ingress traffic tutorial to deploy a sample service that will be exposed outside of the service mesh. The following are relevant snippets from that tutorial. Install a sample service \u00b6 With automatic sidecar injection: $ kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.6/samples/httpbin/httpbin.yaml Otherwise: $ kubectl apply -f < ( istioctl kube-inject -f https://raw.githubusercontent.com/istio/istio/release-1.6/samples/httpbin/httpbin.yaml ) Using a Gateway as a source \u00b6 Create an Istio Gateway: \u00b6 $ cat <=1.0.0. Manifest (for clusters without RBAC enabled) Manifest (for clusters with RBAC enabled) Update existing ExternalDNS Deployment","title":"Configuring ExternalDNS to use the Istio Gateway and/or Istio Virtual Service Source"},{"location":"tutorials/istio/#manifest-for-clusters-without-rbac-enabled","text":"apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service - --source=ingress - --source=istio-gateway # choose one - --source=istio-virtualservice # or both - --domain-filter=external-dns-test.my-org.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones - --provider=aws - --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization - --aws-zone-type=public # only look at public hosted zones (valid values are public, private or no value for both) - --registry=txt - --txt-owner-id=my-identifier","title":"Manifest (for clusters without RBAC enabled)"},{"location":"tutorials/istio/#manifest-for-clusters-with-rbac-enabled","text":"apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" ] - apiGroups : [ \"networking.istio.io\" ] resources : [ \"gateways\" , \"virtualservices\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service - --source=ingress - --source=istio-gateway - --source=istio-virtualservice - --domain-filter=external-dns-test.my-org.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones - --provider=aws - --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization - --aws-zone-type=public # only look at public hosted zones (valid values are public, private or no value for both) - --registry=txt - --txt-owner-id=my-identifier","title":"Manifest (for clusters with RBAC enabled)"},{"location":"tutorials/istio/#update-existing-externaldns-deployment","text":"For clusters with running external-dns , you can just update the deployment. With access to the kube-system namespace, update the existing external-dns deployment. Add a parameter to the arguments of the container to create dns entries with --source=istio-gateway . Execute the following command or update the argument. kubectl patch deployment external-dns --type='json' \\ -p='[{\"op\": \"add\", \"path\": \"/spec/template/spec/containers/0/args/2\", \"value\": \"--source=istio-gateway\" }]' In case the setup uses a clusterrole , just append a new value to the enable the istio group. kubectl patch clusterrole external-dns --type='json' \\ -p='[{\"op\": \"add\", \"path\": \"/rules/4\", \"value\": { \"apiGroups\": [ \"networking.istio.io\"], \"resources\": [\"gateways\"],\"verbs\": [\"get\", \"watch\", \"list\" ]} }]'","title":"Update existing ExternalDNS Deployment"},{"location":"tutorials/istio/#verify-that-istio-gatewayvirtualservice-source-works","text":"Follow the Istio ingress traffic tutorial to deploy a sample service that will be exposed outside of the service mesh. The following are relevant snippets from that tutorial.","title":"Verify that Istio Gateway/VirtualService Source works"},{"location":"tutorials/istio/#install-a-sample-service","text":"With automatic sidecar injection: $ kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.6/samples/httpbin/httpbin.yaml Otherwise: $ kubectl apply -f < ( istioctl kube-inject -f https://raw.githubusercontent.com/istio/istio/release-1.6/samples/httpbin/httpbin.yaml )","title":"Install a sample service"},{"location":"tutorials/istio/#using-a-gateway-as-a-source","text":"","title":"Using a Gateway as a source"},{"location":"tutorials/istio/#create-an-istio-gateway","text":"$ cat <=0.5.5 version of ExternalDNS for this tutorial. Managing DNS with Linode \u00b6 If you want to learn about how to use Linode DNS Manager read the following tutorials: An Introduction to Managing DNS , and general documentation Creating Linode Credentials \u00b6 Generate a new oauth token by following the instructions at Access-and-Authentication The environment variable LINODE_TOKEN will be needed to run ExternalDNS with Linode. Deploy ExternalDNS \u00b6 Connect your kubectl client to the cluster you want to test ExternalDNS with. Then apply one of the following manifests file to deploy ExternalDNS. Manifest (for clusters without RBAC enabled) \u00b6 apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=linode env : - name : LINODE_TOKEN value : \"YOUR_LINODE_API_KEY\" Manifest (for clusters with RBAC enabled) \u00b6 apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=linode env : - name : LINODE_TOKEN value : \"YOUR_LINODE_API_KEY\" Deploying an Nginx Service \u00b6 Create a service file called \u2018nginx.yaml\u2019 with the following contents: apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 --- apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : my-app.example.com spec : selector : app : nginx type : LoadBalancer ports : - protocol : TCP port : 80 targetPort : 80 Note the annotation on the service; use the same hostname as the Linode DNS zone created above. ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records. Create the deployment and service: $ kubectl create -f nginx.yaml Depending where you run your service it can take a little while for your cloud provider to create an external IP for the service. Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize the Linode DNS records. Verifying Linode DNS records \u00b6 Check your Linode UI to view the records for your Linode DNS zone. Click on the zone for the one created above if a different domain was used. This should show the external IP address of the service as the A record for your domain. Cleanup \u00b6 Now that we have verified that ExternalDNS will automatically manage Linode DNS records, we can delete the tutorial\u2019s example: $ kubectl delete service -f nginx.yaml $ kubectl delete service -f externaldns.yaml","title":"Setting up ExternalDNS for Services on Linode"},{"location":"tutorials/linode/#setting-up-externaldns-for-services-on-linode","text":"This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using Linode DNS Manager. Make sure to use >=0.5.5 version of ExternalDNS for this tutorial.","title":"Setting up ExternalDNS for Services on Linode"},{"location":"tutorials/linode/#managing-dns-with-linode","text":"If you want to learn about how to use Linode DNS Manager read the following tutorials: An Introduction to Managing DNS , and general documentation","title":"Managing DNS with Linode"},{"location":"tutorials/linode/#creating-linode-credentials","text":"Generate a new oauth token by following the instructions at Access-and-Authentication The environment variable LINODE_TOKEN will be needed to run ExternalDNS with Linode.","title":"Creating Linode Credentials"},{"location":"tutorials/linode/#deploy-externaldns","text":"Connect your kubectl client to the cluster you want to test ExternalDNS with. Then apply one of the following manifests file to deploy ExternalDNS.","title":"Deploy ExternalDNS"},{"location":"tutorials/linode/#manifest-for-clusters-without-rbac-enabled","text":"apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=linode env : - name : LINODE_TOKEN value : \"YOUR_LINODE_API_KEY\"","title":"Manifest (for clusters without RBAC enabled)"},{"location":"tutorials/linode/#manifest-for-clusters-with-rbac-enabled","text":"apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=linode env : - name : LINODE_TOKEN value : \"YOUR_LINODE_API_KEY\"","title":"Manifest (for clusters with RBAC enabled)"},{"location":"tutorials/linode/#deploying-an-nginx-service","text":"Create a service file called \u2018nginx.yaml\u2019 with the following contents: apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 --- apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : my-app.example.com spec : selector : app : nginx type : LoadBalancer ports : - protocol : TCP port : 80 targetPort : 80 Note the annotation on the service; use the same hostname as the Linode DNS zone created above. ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records. Create the deployment and service: $ kubectl create -f nginx.yaml Depending where you run your service it can take a little while for your cloud provider to create an external IP for the service. Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize the Linode DNS records.","title":"Deploying an Nginx Service"},{"location":"tutorials/linode/#verifying-linode-dns-records","text":"Check your Linode UI to view the records for your Linode DNS zone. Click on the zone for the one created above if a different domain was used. This should show the external IP address of the service as the A record for your domain.","title":"Verifying Linode DNS records"},{"location":"tutorials/linode/#cleanup","text":"Now that we have verified that ExternalDNS will automatically manage Linode DNS records, we can delete the tutorial\u2019s example: $ kubectl delete service -f nginx.yaml $ kubectl delete service -f externaldns.yaml","title":"Cleanup"},{"location":"tutorials/mx-record/","text":"Creating MX record with CRD source \u00b6 You can create and manage MX records with the help of CRD source and DNSEndpoint CRD. Currently, this feature is only supported by aws , azure , and google providers. In order to start managing MX records you need to set the --managed-record-types MX flag. external-dns --source crd --provider {aws|azure|google} --managed-record-types A --managed-record-types CNAME --managed-record-types MX Targets within the CRD need to be specified according to the RFC 1034 (section 3.6.1). Below is an example of example.com DNS MX record which specifies two separate targets with distinct priorities. apiVersion : externaldns.k8s.io/v1alpha1 kind : DNSEndpoint metadata : name : examplemxrecord spec : endpoints : - dnsName : example.com recordTTL : 180 recordType : MX targets : - 10 mailhost1.example.com - 20 mailhost2.example.com","title":"Creating MX record with CRD source"},{"location":"tutorials/mx-record/#creating-mx-record-with-crd-source","text":"You can create and manage MX records with the help of CRD source and DNSEndpoint CRD. Currently, this feature is only supported by aws , azure , and google providers. In order to start managing MX records you need to set the --managed-record-types MX flag. external-dns --source crd --provider {aws|azure|google} --managed-record-types A --managed-record-types CNAME --managed-record-types MX Targets within the CRD need to be specified according to the RFC 1034 (section 3.6.1). Below is an example of example.com DNS MX record which specifies two separate targets with distinct priorities. apiVersion : externaldns.k8s.io/v1alpha1 kind : DNSEndpoint metadata : name : examplemxrecord spec : endpoints : - dnsName : example.com recordTTL : 180 recordType : MX targets : - 10 mailhost1.example.com - 20 mailhost2.example.com","title":"Creating MX record with CRD source"},{"location":"tutorials/nginx-ingress/","text":"Setting up ExternalDNS on GKE with nginx-ingress-controller \u00b6 This tutorial describes how to setup ExternalDNS for usage within a GKE cluster that doesn\u2019t make use of Google\u2019s default ingress controller but rather uses nginx-ingress-controller for that task. Set up your environment \u00b6 Setup your environment to work with Google Cloud Platform. Fill in your values as needed, e.g. target project. $ gcloud config set project \"zalando-external-dns-test\" $ gcloud config set compute/region \"europe-west1\" $ gcloud config set compute/zone \"europe-west1-d\" GKE Node Scopes \u00b6 The following instructions use instance scopes to provide ExternalDNS with the permissions it needs to manage DNS records. Note that since these permissions are associated with the instance, all pods in the cluster will also have these permissions. As such, this approach is not suitable for anything but testing environments. Create a GKE cluster without using the default ingress controller. $ gcloud container clusters create \"external-dns\" \\ --num-nodes 1 \\ --scopes \"https://www.googleapis.com/auth/ndev.clouddns.readwrite\" Create a DNS zone which will contain the managed DNS records. $ gcloud dns managed-zones create \"external-dns-test-gcp-zalan-do\" \\ --dns-name \"external-dns-test.gcp.zalan.do.\" \\ --description \"Automatically managed zone by ExternalDNS\" Make a note of the nameservers that were assigned to your new zone. $ gcloud dns record-sets list \\ --zone \"external-dns-test-gcp-zalan-do\" \\ --name \"external-dns-test.gcp.zalan.do.\" \\ --type NS NAME TYPE TTL DATA external-dns-test.gcp.zalan.do. NS 21600 ns-cloud-e1.googledomains.com.,ns-cloud-e2.googledomains.com.,ns-cloud-e3.googledomains.com.,ns-cloud-e4.googledomains.com. In this case it\u2019s ns-cloud-{e1-e4}.googledomains.com. but your\u2019s could slightly differ, e.g. {a1-a4} , {b1-b4} etc. Tell the parent zone where to find the DNS records for this zone by adding the corresponding NS records there. Assuming the parent zone is \u201cgcp-zalan-do\u201d and the domain is \u201cgcp.zalan.do\u201d and that it\u2019s also hosted at Google we would do the following. $ gcloud dns record-sets transaction start --zone \"gcp-zalan-do\" $ gcloud dns record-sets transaction add ns-cloud-e { 1 ..4 } .googledomains.com. \\ --name \"external-dns-test.gcp.zalan.do.\" --ttl 300 --type NS --zone \"gcp-zalan-do\" $ gcloud dns record-sets transaction execute --zone \"gcp-zalan-do\" Connect your kubectl client to the cluster you just created and bind your GCP user to the cluster admin role in Kubernetes. $ gcloud container clusters get-credentials \"external-dns\" $ kubectl create clusterrolebinding cluster-admin-me \\ --clusterrole = cluster-admin --user = \" $( gcloud config get-value account ) \" Deploy the nginx ingress controller \u00b6 First, you need to deploy the nginx-based ingress controller. It can be deployed in at least two modes: Leveraging a Layer 4 load balancer in front of the nginx proxies or directly targeting pods with hostPorts on your worker nodes. ExternalDNS doesn\u2019t really care and supports both modes. Default Backend \u00b6 The nginx controller uses a default backend that it serves when no Ingress rule matches. This is a separate Service that can be picked by you. We\u2019ll use the default backend that\u2019s used by other ingress controllers for that matter. Apply the following manifests to your cluster to deploy the default backend. apiVersion : v1 kind : Service metadata : name : default-http-backend spec : ports : - port : 80 targetPort : 8080 selector : app : default-http-backend --- apiVersion : apps/v1 kind : Deployment metadata : name : default-http-backend spec : selector : matchLabels : app : default-http-backend template : metadata : labels : app : default-http-backend spec : containers : - name : default-http-backend image : gcr.io/google_containers/defaultbackend:1.3 Without a separate TCP load balancer \u00b6 By default, the controller will update your Ingress objects with the public IPs of the nodes running your nginx controller instances. You should run multiple instances in case of pod or node failure. The controller will do leader election and will put multiple IPs as targets in your Ingress objects in that case. It could also make sense to run it as a DaemonSet. However, we\u2019ll just run a single replica. You have to open the respective ports on all of your worker nodes to allow nginx to receive traffic. $ gcloud compute firewall-rules create \"allow-http\" --allow tcp:80 --source-ranges \"0.0.0.0/0\" --target-tags \"gke-external-dns-9488ba14-node\" $ gcloud compute firewall-rules create \"allow-https\" --allow tcp:443 --source-ranges \"0.0.0.0/0\" --target-tags \"gke-external-dns-9488ba14-node\" Change --target-tags to the corresponding tags of your nodes. You can find them by describing your instances or by looking at the default firewall rules created by GKE for your cluster. Apply the following manifests to your cluster to deploy the nginx-based ingress controller. Note, how it receives a reference to the default backend\u2019s Service and that it listens on hostPorts. (You may have to use hostNetwork: true as well.) apiVersion : apps/v1 kind : Deployment metadata : name : nginx-ingress-controller spec : selector : matchLabels : app : nginx-ingress-controller template : metadata : labels : app : nginx-ingress-controller spec : containers : - name : nginx-ingress-controller image : gcr.io/google_containers/nginx-ingress-controller:0.9.0-beta.3 args : - /nginx-ingress-controller - --default-backend-service=default/default-http-backend env : - name : POD_NAME valueFrom : fieldRef : fieldPath : metadata.name - name : POD_NAMESPACE valueFrom : fieldRef : fieldPath : metadata.namespace ports : - containerPort : 80 hostPort : 80 - containerPort : 443 hostPort : 443 With a separate TCP load balancer \u00b6 However, you can also have the ingress controller proxied by a Kubernetes Service. This will instruct the controller to populate this Service\u2019s external IP as the external IP of the Ingress. This exposes the nginx proxies via a Layer 4 load balancer ( type=LoadBalancer ) which is more reliable than the other method. With that approach, you can run as many nginx proxy instances on your cluster as you like or have them autoscaled. This is the preferred way of running the nginx controller. Apply the following manifests to your cluster. Note, how the controller is receiving an additional flag telling it which Service it should treat as its public endpoint and how it doesn\u2019t need hostPorts anymore. Apply the following manifests to run the controller in this mode. apiVersion : v1 kind : Service metadata : name : nginx-ingress-controller spec : type : LoadBalancer ports : - name : http port : 80 targetPort : 80 - name : https port : 443 targetPort : 443 selector : app : nginx-ingress-controller --- apiVersion : apps/v1 kind : Deployment metadata : name : nginx-ingress-controller spec : selector : matchLabels : app : nginx-ingress-controller template : metadata : labels : app : nginx-ingress-controller spec : containers : - name : nginx-ingress-controller image : gcr.io/google_containers/nginx-ingress-controller:0.9.0-beta.3 args : - /nginx-ingress-controller - --default-backend-service=default/default-http-backend - --publish-service=default/nginx-ingress-controller env : - name : POD_NAME valueFrom : fieldRef : fieldPath : metadata.name - name : POD_NAMESPACE valueFrom : fieldRef : fieldPath : metadata.namespace ports : - containerPort : 80 - containerPort : 443 Deploy ExternalDNS \u00b6 Apply the following manifest file to deploy ExternalDNS. apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=ingress - --domain-filter=external-dns-test.gcp.zalan.do - --provider=google - --google-project=zalando-external-dns-test - --registry=txt - --txt-owner-id=my-identifier Use --dry-run if you want to be extra careful on the first run. Note, that you will not see any records created when you are running in dry-run mode. You can, however, inspect the logs and watch what would have been done. Deploy a sample application \u00b6 Create the following sample application to test that ExternalDNS works. apiVersion : networking.k8s.io/v1 kind : Ingress metadata : name : nginx spec : ingressClassName : nginx rules : - host : via-ingress.external-dns-test.gcp.zalan.do http : paths : - path : / backend : service : name : nginx port : number : 80 pathType : Prefix --- apiVersion : v1 kind : Service metadata : name : nginx spec : ports : - port : 80 targetPort : 80 selector : app : nginx --- apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 After roughly two minutes check that a corresponding DNS record for your Ingress was created. $ gcloud dns record-sets list \\ --zone \"external-dns-test-gcp-zalan-do\" \\ --name \"via-ingress.external-dns-test.gcp.zalan.do.\" \\ --type A NAME TYPE TTL DATA via-ingress.external-dns-test.gcp.zalan.do. A 300 35.187.1.246 Let\u2019s check that we can resolve this DNS name as well. dig +short @ns-cloud-e1.googledomains.com. via-ingress.external-dns-test.gcp.zalan.do. 35.187.1.246 Try with curl as well. $ curl via-ingress.external-dns-test.gcp.zalan.do Welcome to nginx! ... ... Clean up \u00b6 Make sure to delete all Service and Ingress objects before terminating the cluster so all load balancers and DNS entries get cleaned up correctly. $ kubectl delete service nginx-ingress-controller $ kubectl delete ingress nginx Give ExternalDNS some time to clean up the DNS records for you. Then delete the managed zone and cluster. $ gcloud dns managed-zones delete \"external-dns-test-gcp-zalan-do\" $ gcloud container clusters delete \"external-dns\" Also delete the NS records for your removed zone from the parent zone. $ gcloud dns record-sets transaction start --zone \"gcp-zalan-do\" $ gcloud dns record-sets transaction remove ns-cloud-e { 1 ..4 } .googledomains.com. \\ --name \"external-dns-test.gcp.zalan.do.\" --ttl 300 --type NS --zone \"gcp-zalan-do\" $ gcloud dns record-sets transaction execute --zone \"gcp-zalan-do\" GKE with Workload Identity \u00b6 The following instructions use GKE workload identity to provide ExternalDNS with the permissions it needs to manage DNS records. Workload identity is the Google-recommended way to provide GKE workloads access to GCP APIs. Create a GKE cluster with workload identity enabled and without the HttpLoadBalancing add-on. $ gcloud container clusters create external-dns \\ --workload-metadata-from-node = GKE_METADATA_SERVER \\ --identity-namespace = zalando-external-dns-test.svc.id.goog \\ --addons = HorizontalPodAutoscaling Create a GCP service account (GSA) for ExternalDNS and save its email address. $ sa_name = \"Kubernetes external-dns\" $ gcloud iam service-accounts create sa-edns --display-name = \" $sa_name \" $ sa_email = $( gcloud iam service-accounts list --format = 'value(email)' \\ --filter = \"displayName: $sa_name \" ) Bind the ExternalDNS GSA to the DNS admin role. $ gcloud projects add-iam-policy-binding zalando-external-dns-test \\ --member = \"serviceAccount: $sa_email \" --role = roles/dns.admin Link the ExternalDNS GSA to the Kubernetes service account (KSA) that external-dns will run under, i.e., the external-dns KSA in the external-dns namespaces. $ gcloud iam service-accounts add-iam-policy-binding \" $sa_email \" \\ --member = \"serviceAccount:zalando-external-dns-test.svc.id.goog[external-dns/external-dns]\" \\ --role = roles/iam.workloadIdentityUser Create a DNS zone which will contain the managed DNS records. $ gcloud dns managed-zones create external-dns-test-gcp-zalan-do \\ --dns-name = external-dns-test.gcp.zalan.do. \\ --description = \"Automatically managed zone by ExternalDNS\" Make a note of the nameservers that were assigned to your new zone. $ gcloud dns record-sets list \\ --zone = external-dns-test-gcp-zalan-do \\ --name = external-dns-test.gcp.zalan.do. \\ --type NS NAME TYPE TTL DATA external-dns-test.gcp.zalan.do. NS 21600 ns-cloud-e1.googledomains.com.,ns-cloud-e2.googledomains.com.,ns-cloud-e3.googledomains.com.,ns-cloud-e4.googledomains.com. In this case it\u2019s ns-cloud-{e1-e4}.googledomains.com. but your\u2019s could slightly differ, e.g. {a1-a4} , {b1-b4} etc. Tell the parent zone where to find the DNS records for this zone by adding the corresponding NS records there. Assuming the parent zone is \u201cgcp-zalan-do\u201d and the domain is \u201cgcp.zalan.do\u201d and that it\u2019s also hosted at Google we would do the following. $ gcloud dns record-sets transaction start --zone = gcp-zalan-do $ gcloud dns record-sets transaction add ns-cloud-e { 1 ..4 } .googledomains.com. \\ --name = external-dns-test.gcp.zalan.do. --ttl 300 --type NS --zone = gcp-zalan-do $ gcloud dns record-sets transaction execute --zone = gcp-zalan-do Connect your kubectl client to the cluster you just created and bind your GCP user to the cluster admin role in Kubernetes. $ gcloud container clusters get-credentials external-dns $ kubectl create clusterrolebinding cluster-admin-me \\ --clusterrole = cluster-admin --user = \" $( gcloud config get-value account ) \" Deploy ingress-nginx \u00b6 Follow the ingress-nginx GKE installation instructions to deploy it to the cluster. $ kubectl apply -f \\ https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v0.35.0/deploy/static/provider/cloud/deploy.yaml Deploy ExternalDNS \u00b6 Apply the following manifest file to deploy external-dns. apiVersion : v1 kind : Namespace metadata : name : external-dns --- apiVersion : v1 kind : ServiceAccount metadata : name : external-dns namespace : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : external-dns --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns namespace : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - args : - --source=ingress - --domain-filter=external-dns-test.gcp.zalan.do - --provider=google - --google-project=zalando-external-dns-test - --registry=txt - --txt-owner-id=my-identifier image : registry.k8s.io/external-dns/external-dns:v0.13.5 name : external-dns securityContext : fsGroup : 65534 runAsUser : 65534 serviceAccountName : external-dns Then add the proper workload identity annotation to the cert-manager service account. $ kubectl annotate serviceaccount --namespace = external-dns external-dns \\ \"iam.gke.io/gcp-service-account= $sa_email \" Deploy a sample application \u00b6 Create the following sample application to test that ExternalDNS works. apiVersion : networking.k8s.io/v1 kind : Ingress metadata : name : nginx spec : ingressClassName : nginx rules : - host : via-ingress.external-dns-test.gcp.zalan.do http : paths : - path : / backend : service : name : nginx port : number : 80 pathType : Prefix --- apiVersion : v1 kind : Service metadata : name : nginx spec : ports : - port : 80 targetPort : 80 selector : app : nginx --- apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 After roughly two minutes check that a corresponding DNS record for your ingress was created. $ gcloud dns record-sets list \\ --zone \"external-dns-test-gcp-zalan-do\" \\ --name \"via-ingress.external-dns-test.gcp.zalan.do.\" \\ --type A NAME TYPE TTL DATA via-ingress.external-dns-test.gcp.zalan.do. A 300 35.187.1.246 Let\u2019s check that we can resolve this DNS name as well. $ dig +short @ns-cloud-e1.googledomains.com. via-ingress.external-dns-test.gcp.zalan.do. 35.187.1.246 Try with curl as well. $ curl via-ingress.external-dns-test.gcp.zalan.do Welcome to nginx! ... ... Clean up \u00b6 Make sure to delete all service and ingress objects before terminating the cluster so all load balancers and DNS entries get cleaned up correctly. $ kubectl delete service --namespace = ingress-nginx ingress-nginx-controller $ kubectl delete ingress nginx Give ExternalDNS some time to clean up the DNS records for you. Then delete the managed zone and cluster. $ gcloud dns managed-zones delete external-dns-test-gcp-zalan-do $ gcloud container clusters delete external-dns Also delete the NS records for your removed zone from the parent zone. $ gcloud dns record-sets transaction start --zone gcp-zalan-do $ gcloud dns record-sets transaction remove ns-cloud-e { 1 ..4 } .googledomains.com. \\ --name = external-dns-test.gcp.zalan.do. --ttl 300 --type NS --zone = gcp-zalan-do $ gcloud dns record-sets transaction execute --zone = gcp-zalan-do User Demo How-To Blogs and Examples \u00b6 Run external-dns on GKE with workload identity. See Kubernetes, ingress-nginx, cert-manager & external-dns","title":"Setting up ExternalDNS on GKE with nginx-ingress-controller"},{"location":"tutorials/nginx-ingress/#setting-up-externaldns-on-gke-with-nginx-ingress-controller","text":"This tutorial describes how to setup ExternalDNS for usage within a GKE cluster that doesn\u2019t make use of Google\u2019s default ingress controller but rather uses nginx-ingress-controller for that task.","title":"Setting up ExternalDNS on GKE with nginx-ingress-controller"},{"location":"tutorials/nginx-ingress/#set-up-your-environment","text":"Setup your environment to work with Google Cloud Platform. Fill in your values as needed, e.g. target project. $ gcloud config set project \"zalando-external-dns-test\" $ gcloud config set compute/region \"europe-west1\" $ gcloud config set compute/zone \"europe-west1-d\"","title":"Set up your environment"},{"location":"tutorials/nginx-ingress/#gke-node-scopes","text":"The following instructions use instance scopes to provide ExternalDNS with the permissions it needs to manage DNS records. Note that since these permissions are associated with the instance, all pods in the cluster will also have these permissions. As such, this approach is not suitable for anything but testing environments. Create a GKE cluster without using the default ingress controller. $ gcloud container clusters create \"external-dns\" \\ --num-nodes 1 \\ --scopes \"https://www.googleapis.com/auth/ndev.clouddns.readwrite\" Create a DNS zone which will contain the managed DNS records. $ gcloud dns managed-zones create \"external-dns-test-gcp-zalan-do\" \\ --dns-name \"external-dns-test.gcp.zalan.do.\" \\ --description \"Automatically managed zone by ExternalDNS\" Make a note of the nameservers that were assigned to your new zone. $ gcloud dns record-sets list \\ --zone \"external-dns-test-gcp-zalan-do\" \\ --name \"external-dns-test.gcp.zalan.do.\" \\ --type NS NAME TYPE TTL DATA external-dns-test.gcp.zalan.do. NS 21600 ns-cloud-e1.googledomains.com.,ns-cloud-e2.googledomains.com.,ns-cloud-e3.googledomains.com.,ns-cloud-e4.googledomains.com. In this case it\u2019s ns-cloud-{e1-e4}.googledomains.com. but your\u2019s could slightly differ, e.g. {a1-a4} , {b1-b4} etc. Tell the parent zone where to find the DNS records for this zone by adding the corresponding NS records there. Assuming the parent zone is \u201cgcp-zalan-do\u201d and the domain is \u201cgcp.zalan.do\u201d and that it\u2019s also hosted at Google we would do the following. $ gcloud dns record-sets transaction start --zone \"gcp-zalan-do\" $ gcloud dns record-sets transaction add ns-cloud-e { 1 ..4 } .googledomains.com. \\ --name \"external-dns-test.gcp.zalan.do.\" --ttl 300 --type NS --zone \"gcp-zalan-do\" $ gcloud dns record-sets transaction execute --zone \"gcp-zalan-do\" Connect your kubectl client to the cluster you just created and bind your GCP user to the cluster admin role in Kubernetes. $ gcloud container clusters get-credentials \"external-dns\" $ kubectl create clusterrolebinding cluster-admin-me \\ --clusterrole = cluster-admin --user = \" $( gcloud config get-value account ) \"","title":"GKE Node Scopes"},{"location":"tutorials/nginx-ingress/#deploy-the-nginx-ingress-controller","text":"First, you need to deploy the nginx-based ingress controller. It can be deployed in at least two modes: Leveraging a Layer 4 load balancer in front of the nginx proxies or directly targeting pods with hostPorts on your worker nodes. ExternalDNS doesn\u2019t really care and supports both modes.","title":"Deploy the nginx ingress controller"},{"location":"tutorials/nginx-ingress/#default-backend","text":"The nginx controller uses a default backend that it serves when no Ingress rule matches. This is a separate Service that can be picked by you. We\u2019ll use the default backend that\u2019s used by other ingress controllers for that matter. Apply the following manifests to your cluster to deploy the default backend. apiVersion : v1 kind : Service metadata : name : default-http-backend spec : ports : - port : 80 targetPort : 8080 selector : app : default-http-backend --- apiVersion : apps/v1 kind : Deployment metadata : name : default-http-backend spec : selector : matchLabels : app : default-http-backend template : metadata : labels : app : default-http-backend spec : containers : - name : default-http-backend image : gcr.io/google_containers/defaultbackend:1.3","title":"Default Backend"},{"location":"tutorials/nginx-ingress/#without-a-separate-tcp-load-balancer","text":"By default, the controller will update your Ingress objects with the public IPs of the nodes running your nginx controller instances. You should run multiple instances in case of pod or node failure. The controller will do leader election and will put multiple IPs as targets in your Ingress objects in that case. It could also make sense to run it as a DaemonSet. However, we\u2019ll just run a single replica. You have to open the respective ports on all of your worker nodes to allow nginx to receive traffic. $ gcloud compute firewall-rules create \"allow-http\" --allow tcp:80 --source-ranges \"0.0.0.0/0\" --target-tags \"gke-external-dns-9488ba14-node\" $ gcloud compute firewall-rules create \"allow-https\" --allow tcp:443 --source-ranges \"0.0.0.0/0\" --target-tags \"gke-external-dns-9488ba14-node\" Change --target-tags to the corresponding tags of your nodes. You can find them by describing your instances or by looking at the default firewall rules created by GKE for your cluster. Apply the following manifests to your cluster to deploy the nginx-based ingress controller. Note, how it receives a reference to the default backend\u2019s Service and that it listens on hostPorts. (You may have to use hostNetwork: true as well.) apiVersion : apps/v1 kind : Deployment metadata : name : nginx-ingress-controller spec : selector : matchLabels : app : nginx-ingress-controller template : metadata : labels : app : nginx-ingress-controller spec : containers : - name : nginx-ingress-controller image : gcr.io/google_containers/nginx-ingress-controller:0.9.0-beta.3 args : - /nginx-ingress-controller - --default-backend-service=default/default-http-backend env : - name : POD_NAME valueFrom : fieldRef : fieldPath : metadata.name - name : POD_NAMESPACE valueFrom : fieldRef : fieldPath : metadata.namespace ports : - containerPort : 80 hostPort : 80 - containerPort : 443 hostPort : 443","title":"Without a separate TCP load balancer"},{"location":"tutorials/nginx-ingress/#with-a-separate-tcp-load-balancer","text":"However, you can also have the ingress controller proxied by a Kubernetes Service. This will instruct the controller to populate this Service\u2019s external IP as the external IP of the Ingress. This exposes the nginx proxies via a Layer 4 load balancer ( type=LoadBalancer ) which is more reliable than the other method. With that approach, you can run as many nginx proxy instances on your cluster as you like or have them autoscaled. This is the preferred way of running the nginx controller. Apply the following manifests to your cluster. Note, how the controller is receiving an additional flag telling it which Service it should treat as its public endpoint and how it doesn\u2019t need hostPorts anymore. Apply the following manifests to run the controller in this mode. apiVersion : v1 kind : Service metadata : name : nginx-ingress-controller spec : type : LoadBalancer ports : - name : http port : 80 targetPort : 80 - name : https port : 443 targetPort : 443 selector : app : nginx-ingress-controller --- apiVersion : apps/v1 kind : Deployment metadata : name : nginx-ingress-controller spec : selector : matchLabels : app : nginx-ingress-controller template : metadata : labels : app : nginx-ingress-controller spec : containers : - name : nginx-ingress-controller image : gcr.io/google_containers/nginx-ingress-controller:0.9.0-beta.3 args : - /nginx-ingress-controller - --default-backend-service=default/default-http-backend - --publish-service=default/nginx-ingress-controller env : - name : POD_NAME valueFrom : fieldRef : fieldPath : metadata.name - name : POD_NAMESPACE valueFrom : fieldRef : fieldPath : metadata.namespace ports : - containerPort : 80 - containerPort : 443","title":"With a separate TCP load balancer"},{"location":"tutorials/nginx-ingress/#deploy-externaldns","text":"Apply the following manifest file to deploy ExternalDNS. apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=ingress - --domain-filter=external-dns-test.gcp.zalan.do - --provider=google - --google-project=zalando-external-dns-test - --registry=txt - --txt-owner-id=my-identifier Use --dry-run if you want to be extra careful on the first run. Note, that you will not see any records created when you are running in dry-run mode. You can, however, inspect the logs and watch what would have been done.","title":"Deploy ExternalDNS"},{"location":"tutorials/nginx-ingress/#deploy-a-sample-application","text":"Create the following sample application to test that ExternalDNS works. apiVersion : networking.k8s.io/v1 kind : Ingress metadata : name : nginx spec : ingressClassName : nginx rules : - host : via-ingress.external-dns-test.gcp.zalan.do http : paths : - path : / backend : service : name : nginx port : number : 80 pathType : Prefix --- apiVersion : v1 kind : Service metadata : name : nginx spec : ports : - port : 80 targetPort : 80 selector : app : nginx --- apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 After roughly two minutes check that a corresponding DNS record for your Ingress was created. $ gcloud dns record-sets list \\ --zone \"external-dns-test-gcp-zalan-do\" \\ --name \"via-ingress.external-dns-test.gcp.zalan.do.\" \\ --type A NAME TYPE TTL DATA via-ingress.external-dns-test.gcp.zalan.do. A 300 35.187.1.246 Let\u2019s check that we can resolve this DNS name as well. dig +short @ns-cloud-e1.googledomains.com. via-ingress.external-dns-test.gcp.zalan.do. 35.187.1.246 Try with curl as well. $ curl via-ingress.external-dns-test.gcp.zalan.do Welcome to nginx! ... ... ","title":"Deploy a sample application"},{"location":"tutorials/nginx-ingress/#clean-up","text":"Make sure to delete all Service and Ingress objects before terminating the cluster so all load balancers and DNS entries get cleaned up correctly. $ kubectl delete service nginx-ingress-controller $ kubectl delete ingress nginx Give ExternalDNS some time to clean up the DNS records for you. Then delete the managed zone and cluster. $ gcloud dns managed-zones delete \"external-dns-test-gcp-zalan-do\" $ gcloud container clusters delete \"external-dns\" Also delete the NS records for your removed zone from the parent zone. $ gcloud dns record-sets transaction start --zone \"gcp-zalan-do\" $ gcloud dns record-sets transaction remove ns-cloud-e { 1 ..4 } .googledomains.com. \\ --name \"external-dns-test.gcp.zalan.do.\" --ttl 300 --type NS --zone \"gcp-zalan-do\" $ gcloud dns record-sets transaction execute --zone \"gcp-zalan-do\"","title":"Clean up"},{"location":"tutorials/nginx-ingress/#gke-with-workload-identity","text":"The following instructions use GKE workload identity to provide ExternalDNS with the permissions it needs to manage DNS records. Workload identity is the Google-recommended way to provide GKE workloads access to GCP APIs. Create a GKE cluster with workload identity enabled and without the HttpLoadBalancing add-on. $ gcloud container clusters create external-dns \\ --workload-metadata-from-node = GKE_METADATA_SERVER \\ --identity-namespace = zalando-external-dns-test.svc.id.goog \\ --addons = HorizontalPodAutoscaling Create a GCP service account (GSA) for ExternalDNS and save its email address. $ sa_name = \"Kubernetes external-dns\" $ gcloud iam service-accounts create sa-edns --display-name = \" $sa_name \" $ sa_email = $( gcloud iam service-accounts list --format = 'value(email)' \\ --filter = \"displayName: $sa_name \" ) Bind the ExternalDNS GSA to the DNS admin role. $ gcloud projects add-iam-policy-binding zalando-external-dns-test \\ --member = \"serviceAccount: $sa_email \" --role = roles/dns.admin Link the ExternalDNS GSA to the Kubernetes service account (KSA) that external-dns will run under, i.e., the external-dns KSA in the external-dns namespaces. $ gcloud iam service-accounts add-iam-policy-binding \" $sa_email \" \\ --member = \"serviceAccount:zalando-external-dns-test.svc.id.goog[external-dns/external-dns]\" \\ --role = roles/iam.workloadIdentityUser Create a DNS zone which will contain the managed DNS records. $ gcloud dns managed-zones create external-dns-test-gcp-zalan-do \\ --dns-name = external-dns-test.gcp.zalan.do. \\ --description = \"Automatically managed zone by ExternalDNS\" Make a note of the nameservers that were assigned to your new zone. $ gcloud dns record-sets list \\ --zone = external-dns-test-gcp-zalan-do \\ --name = external-dns-test.gcp.zalan.do. \\ --type NS NAME TYPE TTL DATA external-dns-test.gcp.zalan.do. NS 21600 ns-cloud-e1.googledomains.com.,ns-cloud-e2.googledomains.com.,ns-cloud-e3.googledomains.com.,ns-cloud-e4.googledomains.com. In this case it\u2019s ns-cloud-{e1-e4}.googledomains.com. but your\u2019s could slightly differ, e.g. {a1-a4} , {b1-b4} etc. Tell the parent zone where to find the DNS records for this zone by adding the corresponding NS records there. Assuming the parent zone is \u201cgcp-zalan-do\u201d and the domain is \u201cgcp.zalan.do\u201d and that it\u2019s also hosted at Google we would do the following. $ gcloud dns record-sets transaction start --zone = gcp-zalan-do $ gcloud dns record-sets transaction add ns-cloud-e { 1 ..4 } .googledomains.com. \\ --name = external-dns-test.gcp.zalan.do. --ttl 300 --type NS --zone = gcp-zalan-do $ gcloud dns record-sets transaction execute --zone = gcp-zalan-do Connect your kubectl client to the cluster you just created and bind your GCP user to the cluster admin role in Kubernetes. $ gcloud container clusters get-credentials external-dns $ kubectl create clusterrolebinding cluster-admin-me \\ --clusterrole = cluster-admin --user = \" $( gcloud config get-value account ) \"","title":"GKE with Workload Identity"},{"location":"tutorials/nginx-ingress/#deploy-ingress-nginx","text":"Follow the ingress-nginx GKE installation instructions to deploy it to the cluster. $ kubectl apply -f \\ https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v0.35.0/deploy/static/provider/cloud/deploy.yaml","title":"Deploy ingress-nginx"},{"location":"tutorials/nginx-ingress/#deploy-externaldns_1","text":"Apply the following manifest file to deploy external-dns. apiVersion : v1 kind : Namespace metadata : name : external-dns --- apiVersion : v1 kind : ServiceAccount metadata : name : external-dns namespace : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : external-dns --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns namespace : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - args : - --source=ingress - --domain-filter=external-dns-test.gcp.zalan.do - --provider=google - --google-project=zalando-external-dns-test - --registry=txt - --txt-owner-id=my-identifier image : registry.k8s.io/external-dns/external-dns:v0.13.5 name : external-dns securityContext : fsGroup : 65534 runAsUser : 65534 serviceAccountName : external-dns Then add the proper workload identity annotation to the cert-manager service account. $ kubectl annotate serviceaccount --namespace = external-dns external-dns \\ \"iam.gke.io/gcp-service-account= $sa_email \"","title":"Deploy ExternalDNS"},{"location":"tutorials/nginx-ingress/#deploy-a-sample-application_1","text":"Create the following sample application to test that ExternalDNS works. apiVersion : networking.k8s.io/v1 kind : Ingress metadata : name : nginx spec : ingressClassName : nginx rules : - host : via-ingress.external-dns-test.gcp.zalan.do http : paths : - path : / backend : service : name : nginx port : number : 80 pathType : Prefix --- apiVersion : v1 kind : Service metadata : name : nginx spec : ports : - port : 80 targetPort : 80 selector : app : nginx --- apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 After roughly two minutes check that a corresponding DNS record for your ingress was created. $ gcloud dns record-sets list \\ --zone \"external-dns-test-gcp-zalan-do\" \\ --name \"via-ingress.external-dns-test.gcp.zalan.do.\" \\ --type A NAME TYPE TTL DATA via-ingress.external-dns-test.gcp.zalan.do. A 300 35.187.1.246 Let\u2019s check that we can resolve this DNS name as well. $ dig +short @ns-cloud-e1.googledomains.com. via-ingress.external-dns-test.gcp.zalan.do. 35.187.1.246 Try with curl as well. $ curl via-ingress.external-dns-test.gcp.zalan.do Welcome to nginx! ... ... ","title":"Deploy a sample application"},{"location":"tutorials/nginx-ingress/#clean-up_1","text":"Make sure to delete all service and ingress objects before terminating the cluster so all load balancers and DNS entries get cleaned up correctly. $ kubectl delete service --namespace = ingress-nginx ingress-nginx-controller $ kubectl delete ingress nginx Give ExternalDNS some time to clean up the DNS records for you. Then delete the managed zone and cluster. $ gcloud dns managed-zones delete external-dns-test-gcp-zalan-do $ gcloud container clusters delete external-dns Also delete the NS records for your removed zone from the parent zone. $ gcloud dns record-sets transaction start --zone gcp-zalan-do $ gcloud dns record-sets transaction remove ns-cloud-e { 1 ..4 } .googledomains.com. \\ --name = external-dns-test.gcp.zalan.do. --ttl 300 --type NS --zone = gcp-zalan-do $ gcloud dns record-sets transaction execute --zone = gcp-zalan-do","title":"Clean up"},{"location":"tutorials/nginx-ingress/#user-demo-how-to-blogs-and-examples","text":"Run external-dns on GKE with workload identity. See Kubernetes, ingress-nginx, cert-manager & external-dns","title":"User Demo How-To Blogs and Examples"},{"location":"tutorials/nodes/","text":"Configuring ExternalDNS to use Cluster Nodes as Source \u00b6 This tutorial describes how to configure ExternalDNS to use the cluster nodes as source. Using nodes ( --source=node ) as source is possible to synchronize a DNS zone with the nodes of a cluster. The node source adds an A record per each node externalIP (if not found, any IPv4 internalIP is used instead). It also adds an AAAA record per each node IPv6 internalIP . The TTL of the records can be set with the external-dns.alpha.kubernetes.io/ttl node annotation. Manifest (for cluster without RBAC enabled) \u00b6 --- apiVersion: apps/v1 kind: Deployment metadata: name: external-dns spec: strategy: type: Recreate selector: matchLabels: app: external-dns template: metadata: labels: app: external-dns spec: serviceAccountName: external-dns containers: - name: external-dns image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=node # will use nodes as source - --provider=aws - --zone-name-filter=external-dns-test.my-org.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones - --domain-filter=external-dns-test.my-org.com - --aws-zone-type=public - --registry=txt - --fqdn-template={{.Name}}.external-dns-test.my-org.com - --txt-owner-id=my-identifier - --policy=sync - --log-level=debug Manifest (for cluster with RBAC enabled) \u00b6 apiVersion: v1 kind: ServiceAccount metadata: name: external-dns --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: external-dns rules: - apiGroups: [\"route.openshift.io\"] resources: [\"routes\"] verbs: [\"get\", \"watch\", \"list\"] - apiGroups: [\"\"] resources: [\"services\",\"endpoints\",\"pods\"] verbs: [\"get\",\"watch\",\"list\"] - apiGroups: [\"extensions\",\"networking.k8s.io\"] resources: [\"ingresses\"] verbs: [\"get\",\"watch\",\"list\"] - apiGroups: [\"\"] resources: [\"nodes\"] verbs: [\"get\", \"watch\", \"list\"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: external-dns-viewer roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: external-dns subjects: - kind: ServiceAccount name: external-dns namespace: external-dns --- apiVersion: apps/v1 kind: Deployment metadata: name: external-dns spec: strategy: type: Recreate selector: matchLabels: app: external-dns template: metadata: labels: app: external-dns spec: serviceAccountName: external-dns containers: - name: external-dns image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=node # will use nodes as source - --provider=aws - --zone-name-filter=external-dns-test.my-org.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones - --domain-filter=external-dns-test.my-org.com - --aws-zone-type=public - --registry=txt - --fqdn-template={{.Name}}.external-dns-test.my-org.com - --txt-owner-id=my-identifier - --policy=sync - --log-level=debug","title":"Configuring ExternalDNS to use Cluster Nodes as Source"},{"location":"tutorials/nodes/#configuring-externaldns-to-use-cluster-nodes-as-source","text":"This tutorial describes how to configure ExternalDNS to use the cluster nodes as source. Using nodes ( --source=node ) as source is possible to synchronize a DNS zone with the nodes of a cluster. The node source adds an A record per each node externalIP (if not found, any IPv4 internalIP is used instead). It also adds an AAAA record per each node IPv6 internalIP . The TTL of the records can be set with the external-dns.alpha.kubernetes.io/ttl node annotation.","title":"Configuring ExternalDNS to use Cluster Nodes as Source"},{"location":"tutorials/nodes/#manifest-for-cluster-without-rbac-enabled","text":"--- apiVersion: apps/v1 kind: Deployment metadata: name: external-dns spec: strategy: type: Recreate selector: matchLabels: app: external-dns template: metadata: labels: app: external-dns spec: serviceAccountName: external-dns containers: - name: external-dns image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=node # will use nodes as source - --provider=aws - --zone-name-filter=external-dns-test.my-org.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones - --domain-filter=external-dns-test.my-org.com - --aws-zone-type=public - --registry=txt - --fqdn-template={{.Name}}.external-dns-test.my-org.com - --txt-owner-id=my-identifier - --policy=sync - --log-level=debug","title":"Manifest (for cluster without RBAC enabled)"},{"location":"tutorials/nodes/#manifest-for-cluster-with-rbac-enabled","text":"apiVersion: v1 kind: ServiceAccount metadata: name: external-dns --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: external-dns rules: - apiGroups: [\"route.openshift.io\"] resources: [\"routes\"] verbs: [\"get\", \"watch\", \"list\"] - apiGroups: [\"\"] resources: [\"services\",\"endpoints\",\"pods\"] verbs: [\"get\",\"watch\",\"list\"] - apiGroups: [\"extensions\",\"networking.k8s.io\"] resources: [\"ingresses\"] verbs: [\"get\",\"watch\",\"list\"] - apiGroups: [\"\"] resources: [\"nodes\"] verbs: [\"get\", \"watch\", \"list\"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: external-dns-viewer roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: external-dns subjects: - kind: ServiceAccount name: external-dns namespace: external-dns --- apiVersion: apps/v1 kind: Deployment metadata: name: external-dns spec: strategy: type: Recreate selector: matchLabels: app: external-dns template: metadata: labels: app: external-dns spec: serviceAccountName: external-dns containers: - name: external-dns image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --source=node # will use nodes as source - --provider=aws - --zone-name-filter=external-dns-test.my-org.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones - --domain-filter=external-dns-test.my-org.com - --aws-zone-type=public - --registry=txt - --fqdn-template={{.Name}}.external-dns-test.my-org.com - --txt-owner-id=my-identifier - --policy=sync - --log-level=debug","title":"Manifest (for cluster with RBAC enabled)"},{"location":"tutorials/ns-record/","text":"Creating NS record with CRD source \u00b6 You can create NS records with the help of CRD source and DNSEndpoint CRD. Consider the following example apiVersion : externaldns.k8s.io/v1alpha1 kind : DNSEndpoint metadata : name : ns-record spec : endpoints : - dnsName : zone.example.com recordTTL : 300 recordType : NS targets : - ns1.example.com - ns2.example.com After instantiation of this Custom Resource external-dns will create NS record with the help of configured provider, e.g. aws","title":"Creating NS record with CRD source"},{"location":"tutorials/ns-record/#creating-ns-record-with-crd-source","text":"You can create NS records with the help of CRD source and DNSEndpoint CRD. Consider the following example apiVersion : externaldns.k8s.io/v1alpha1 kind : DNSEndpoint metadata : name : ns-record spec : endpoints : - dnsName : zone.example.com recordTTL : 300 recordType : NS targets : - ns1.example.com - ns2.example.com After instantiation of this Custom Resource external-dns will create NS record with the help of configured provider, e.g. aws","title":"Creating NS record with CRD source"},{"location":"tutorials/ns1/","text":"Setting up ExternalDNS for Services on NS1 \u00b6 This tutorial describes how to setup ExternalDNS for use within a Kubernetes cluster using NS1 DNS. Make sure to use >=0.5 version of ExternalDNS for this tutorial. Creating a zone with NS1 DNS \u00b6 If you are new to NS1, we recommend you first read the following instructions for creating a zone. Creating a zone using the NS1 portal Creating a zone using the NS1 API Creating NS1 Credentials \u00b6 All NS1 products are API-first, meaning everything that can be done on the portal\u2014including managing zones and records, data sources and feeds, and account settings and users\u2014can be done via API. The NS1 API is a standard REST API with JSON responses. The environment var NS1_APIKEY will be needed to run ExternalDNS with NS1. To add or delete an API key \u00b6 Log into the NS1 portal at my.nsone.net . Click your username in the upper-right corner, and navigate to Account Settings > Users & Teams . Navigate to the API Keys tab, and click Add Key . Enter the name of the application and modify permissions and settings as desired. Once complete, click Create Key . The new API key appears in the list. Note: Set the permissions for your API keys just as you would for a user or team associated with your organization\u2019s NS1 account. For more information, refer to the article Creating and Managing API Keys in the NS1 Knowledge Base. Deploy ExternalDNS \u00b6 Connect your kubectl client to the cluster with which you want to test ExternalDNS, and then apply one of the following manifest files for deployment: Manifest (for clusters without RBAC enabled) \u00b6 apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=ns1 env : - name : NS1_APIKEY value : \"YOUR_NS1_API_KEY\" Manifest (for clusters with RBAC enabled) \u00b6 apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=ns1 env : - name : NS1_APIKEY value : \"YOUR_NS1_API_KEY\" Deploying an Nginx Service \u00b6 Create a service file called \u2018nginx.yaml\u2019 with the following contents: apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 --- apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : example.com external-dns.alpha.kubernetes.io/ttl : \"120\" #optional spec : selector : app : nginx type : LoadBalancer ports : - protocol : TCP port : 80 targetPort : 80 A note about annotations Verify that the annotation on the service uses the same hostname as the NS1 DNS zone created above. The annotation may also be a subdomain of the DNS zone (e.g. \u2018www.example.com\u2019). The TTL annotation can be used to configure the TTL on DNS records managed by ExternalDNS and is optional. If this annotation is not set, the TTL on records managed by ExternalDNS will default to 10. ExternalDNS uses the hostname annotation to determine which services should be registered with DNS. Removing the hostname annotation will cause ExternalDNS to remove the corresponding DNS records. Create the deployment and service \u00b6 $ kubectl create -f nginx.yaml Depending on where you run your service, it may take some time for your cloud provider to create an external IP for the service. Once an external IP is assigned, ExternalDNS detects the new service IP address and synchronizes the NS1 DNS records. Verifying NS1 DNS records \u00b6 Use the NS1 portal or API to verify that the A record for your domain shows the external IP address of the services. Cleanup \u00b6 Once you successfully configure and verify record management via ExternalDNS, you can delete the tutorial\u2019s example: $ kubectl delete -f nginx.yaml $ kubectl delete -f externaldns.yaml","title":"Setting up ExternalDNS for Services on NS1"},{"location":"tutorials/ns1/#setting-up-externaldns-for-services-on-ns1","text":"This tutorial describes how to setup ExternalDNS for use within a Kubernetes cluster using NS1 DNS. Make sure to use >=0.5 version of ExternalDNS for this tutorial.","title":"Setting up ExternalDNS for Services on NS1"},{"location":"tutorials/ns1/#creating-a-zone-with-ns1-dns","text":"If you are new to NS1, we recommend you first read the following instructions for creating a zone. Creating a zone using the NS1 portal Creating a zone using the NS1 API","title":"Creating a zone with NS1 DNS"},{"location":"tutorials/ns1/#creating-ns1-credentials","text":"All NS1 products are API-first, meaning everything that can be done on the portal\u2014including managing zones and records, data sources and feeds, and account settings and users\u2014can be done via API. The NS1 API is a standard REST API with JSON responses. The environment var NS1_APIKEY will be needed to run ExternalDNS with NS1.","title":"Creating NS1 Credentials"},{"location":"tutorials/ns1/#to-add-or-delete-an-api-key","text":"Log into the NS1 portal at my.nsone.net . Click your username in the upper-right corner, and navigate to Account Settings > Users & Teams . Navigate to the API Keys tab, and click Add Key . Enter the name of the application and modify permissions and settings as desired. Once complete, click Create Key . The new API key appears in the list. Note: Set the permissions for your API keys just as you would for a user or team associated with your organization\u2019s NS1 account. For more information, refer to the article Creating and Managing API Keys in the NS1 Knowledge Base.","title":"To add or delete an API key"},{"location":"tutorials/ns1/#deploy-externaldns","text":"Connect your kubectl client to the cluster with which you want to test ExternalDNS, and then apply one of the following manifest files for deployment:","title":"Deploy ExternalDNS"},{"location":"tutorials/ns1/#manifest-for-clusters-without-rbac-enabled","text":"apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=ns1 env : - name : NS1_APIKEY value : \"YOUR_NS1_API_KEY\"","title":"Manifest (for clusters without RBAC enabled)"},{"location":"tutorials/ns1/#manifest-for-clusters-with-rbac-enabled","text":"apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=ns1 env : - name : NS1_APIKEY value : \"YOUR_NS1_API_KEY\"","title":"Manifest (for clusters with RBAC enabled)"},{"location":"tutorials/ns1/#deploying-an-nginx-service","text":"Create a service file called \u2018nginx.yaml\u2019 with the following contents: apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 --- apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : example.com external-dns.alpha.kubernetes.io/ttl : \"120\" #optional spec : selector : app : nginx type : LoadBalancer ports : - protocol : TCP port : 80 targetPort : 80 A note about annotations Verify that the annotation on the service uses the same hostname as the NS1 DNS zone created above. The annotation may also be a subdomain of the DNS zone (e.g. \u2018www.example.com\u2019). The TTL annotation can be used to configure the TTL on DNS records managed by ExternalDNS and is optional. If this annotation is not set, the TTL on records managed by ExternalDNS will default to 10. ExternalDNS uses the hostname annotation to determine which services should be registered with DNS. Removing the hostname annotation will cause ExternalDNS to remove the corresponding DNS records.","title":"Deploying an Nginx Service"},{"location":"tutorials/ns1/#create-the-deployment-and-service","text":"$ kubectl create -f nginx.yaml Depending on where you run your service, it may take some time for your cloud provider to create an external IP for the service. Once an external IP is assigned, ExternalDNS detects the new service IP address and synchronizes the NS1 DNS records.","title":"Create the deployment and service"},{"location":"tutorials/ns1/#verifying-ns1-dns-records","text":"Use the NS1 portal or API to verify that the A record for your domain shows the external IP address of the services.","title":"Verifying NS1 DNS records"},{"location":"tutorials/ns1/#cleanup","text":"Once you successfully configure and verify record management via ExternalDNS, you can delete the tutorial\u2019s example: $ kubectl delete -f nginx.yaml $ kubectl delete -f externaldns.yaml","title":"Cleanup"},{"location":"tutorials/openshift/","text":"Configuring ExternalDNS to use the OpenShift Route Source \u00b6 This tutorial describes how to configure ExternalDNS to use the OpenShift Route source. It is meant to supplement the other provider-specific setup tutorials. For OCP 4.x \u00b6 In OCP 4.x, if you have multiple OpenShift ingress controllers then you must specify an ingress controller name (also called router name), you can get it from the route\u2019s status.ingress[*].routerName field. If you don\u2019t specify a router name when you have multiple ingress controllers in your cluster then the first router from the route\u2019s status.ingress will be used. Note that the router must have admitted the route in order to be selected. Once the router is known, ExternalDNS will use this router\u2019s canonical hostname as the target for the CNAME record. Starting from OCP 4.10 you can use ExternalDNS Operator to manage ExternalDNS instances. Example of its custom resource for AWS provider: apiVersion : externaldns.olm.openshift.io/v1alpha1 kind : ExternalDNS metadata : name : sample spec : provider : type : AWS source : openshiftRouteOptions : routerName : default type : OpenShiftRoute zones : - Z05387772BD5723IZFRX3 This will create an ExternalDNS POD with the following container args in external-dns namespace: spec: containers: - args: - --metrics-address=127.0.0.1:7979 - --txt-owner-id=external-dns-sample - --provider=aws - --source=openshift-route - --policy=sync - --registry=txt - --log-level=debug - --zone-id-filter=Z05387772BD5723IZFRX3 - --openshift-router-name=default - --txt-prefix=external-dns- For OCP 3.11 environment \u00b6 Prepare ROUTER_CANONICAL_HOSTNAME in default/router deployment \u00b6 Read and go through Finding the Host Name of the Router . If no ROUTER_CANONICAL_HOSTNAME is set, you must annotate each route with external-dns.alpha.kubernetes.io/target! Manifest (for clusters without RBAC enabled) \u00b6 apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=openshift-route - --domain-filter=external-dns-test.my-org.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones - --provider=aws - --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization - --aws-zone-type=public # only look at public hosted zones (valid values are public, private or no value for both) - --registry=txt - --txt-owner-id=my-identifier Manifest (for clusters with RBAC enabled) \u00b6 apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" ] - apiGroups : [ \"route.openshift.io\" ] resources : [ \"routes\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=openshift-route - --domain-filter=external-dns-test.my-org.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones - --provider=aws - --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization - --aws-zone-type=public # only look at public hosted zones (valid values are public, private or no value for both) - --registry=txt - --txt-owner-id=my-identifier Verify External DNS works (OpenShift Route example) \u00b6 The following instructions are based on the Hello Openshift . Install a sample service and expose it \u00b6 $ oc apply -f - < to manage dns in compartment id You\u2019ll also need to add the --oci-auth-instance-principal flag to enable this type of authentication. Finally, you\u2019ll need to add the --oci-compartment-ocid=ocid1.compartment.oc1... flag to provide the OCID of the compartment containing the zone to be managed. For more information about OCI IAM instance principals, see the documentation here . For more information about OCI IAM policy details for the DNS service, see the documentation here . Manifest (for clusters with RBAC enabled) \u00b6 Apply the following manifest to deploy ExternalDNS. apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service - --source=ingress - --provider=oci - --policy=upsert-only # prevent ExternalDNS from deleting any records, omit to enable full synchronization - --txt-owner-id=my-identifier volumeMounts : - name : config mountPath : /etc/kubernetes/ volumes : - name : config secret : secretName : external-dns-config Verify ExternalDNS works (Service example) \u00b6 Create the following sample application to test that ExternalDNS works. For services ExternalDNS will look for the annotation external-dns.alpha.kubernetes.io/hostname on the service and use the corresponding value. apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : example.com spec : type : LoadBalancer ports : - port : 80 name : http targetPort : 80 selector : app : nginx --- apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 name : http Apply the manifest above and wait roughly two minutes and check that a corresponding DNS record for your service was created. $ kubectl apply -f nginx.yaml","title":"Setting up ExternalDNS for Oracle Cloud Infrastructure (OCI)"},{"location":"tutorials/oracle/#setting-up-externaldns-for-oracle-cloud-infrastructure-oci","text":"This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using OCI DNS. Make sure to use the latest version of ExternalDNS for this tutorial.","title":"Setting up ExternalDNS for Oracle Cloud Infrastructure (OCI)"},{"location":"tutorials/oracle/#creating-an-oci-dns-zone","text":"Create a DNS zone which will contain the managed DNS records. Let\u2019s use example.com as a reference here. Make note of the OCID of the compartment in which you created the zone; you\u2019ll need to provide that later. For more information about OCI DNS see the documentation here .","title":"Creating an OCI DNS Zone"},{"location":"tutorials/oracle/#deploy-externaldns","text":"Connect your kubectl client to the cluster you want to test ExternalDNS with. The OCI provider supports two authentication options: key-based and instance principals.","title":"Deploy ExternalDNS"},{"location":"tutorials/oracle/#key-based","text":"We first need to create a config file containing the information needed to connect with the OCI API. Create a new file (oci.yaml) and modify the contents to match the example below. Be sure to adjust the values to match your own credentials, and the OCID of the compartment containing the zone: auth : region : us-phoenix-1 tenancy : ocid1.tenancy.oc1... user : ocid1.user.oc1... key : | -----BEGIN RSA PRIVATE KEY----- -----END RSA PRIVATE KEY----- fingerprint : af:81:71:8e... # Omit if there is not a password for the key passphrase : Tx1jRk... compartment : ocid1.compartment.oc1... Create a secret using the config file above: $ kubectl create secret generic external-dns-config --from-file = oci.yaml","title":"Key-based"},{"location":"tutorials/oracle/#oci-iam-instance-principal","text":"If you\u2019re running ExternalDNS within OCI, you can use OCI IAM instance principals to authenticate with OCI. This obviates the need to create the secret with your credentials. You\u2019ll need to ensure an OCI IAM policy exists with a statement granting the manage dns permission on zones and records in the target compartment to the dynamic group covering your instance running ExternalDNS. E.g.: Allow dynamic-group to manage dns in compartment id You\u2019ll also need to add the --oci-auth-instance-principal flag to enable this type of authentication. Finally, you\u2019ll need to add the --oci-compartment-ocid=ocid1.compartment.oc1... flag to provide the OCID of the compartment containing the zone to be managed. For more information about OCI IAM instance principals, see the documentation here . For more information about OCI IAM policy details for the DNS service, see the documentation here .","title":"OCI IAM Instance Principal"},{"location":"tutorials/oracle/#manifest-for-clusters-with-rbac-enabled","text":"Apply the following manifest to deploy ExternalDNS. apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service - --source=ingress - --provider=oci - --policy=upsert-only # prevent ExternalDNS from deleting any records, omit to enable full synchronization - --txt-owner-id=my-identifier volumeMounts : - name : config mountPath : /etc/kubernetes/ volumes : - name : config secret : secretName : external-dns-config","title":"Manifest (for clusters with RBAC enabled)"},{"location":"tutorials/oracle/#verify-externaldns-works-service-example","text":"Create the following sample application to test that ExternalDNS works. For services ExternalDNS will look for the annotation external-dns.alpha.kubernetes.io/hostname on the service and use the corresponding value. apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : example.com spec : type : LoadBalancer ports : - port : 80 name : http targetPort : 80 selector : app : nginx --- apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 name : http Apply the manifest above and wait roughly two minutes and check that a corresponding DNS record for your service was created. $ kubectl apply -f nginx.yaml","title":"Verify ExternalDNS works (Service example)"},{"location":"tutorials/ovh/","text":"Setting up ExternalDNS for Services on OVH \u00b6 This tutorial describes how to setup ExternalDNS for use within a Kubernetes cluster using OVH DNS. Make sure to use >=0.6 version of ExternalDNS for this tutorial. Creating a zone with OVH DNS \u00b6 If you are new to OVH, we recommend you first read the following instructions for creating a zone. Creating a zone using the OVH manager Creating a zone using the OVH API Creating OVH Credentials \u00b6 You first need to create an OVH application. Using the OVH documentation you will have your Application key and Application secret And you will need to generate your consumer key, here the permissions needed : - GET on /domain/zone - GET on /domain/zone/*/record - GET on /domain/zone/*/record/* - POST on /domain/zone/*/record - DELETE on /domain/zone/*/record/* - GET on /domain/zone/*/soa - POST on /domain/zone/*/refresh You can use the following curl request to generate & validated your Consumer key curl -XPOST -H \"X-Ovh-Application: \" -H \"Content-type: application/json\" https://eu.api.ovh.com/1.0/auth/credential -d '{ \"accessRules\": [ { \"method\": \"GET\", \"path\": \"/domain/zone\" }, { \"method\": \"GET\", \"path\": \"/domain/zone/*/soa\" }, { \"method\": \"GET\", \"path\": \"/domain/zone/*/record\" }, { \"method\": \"GET\", \"path\": \"/domain/zone/*/record/*\" }, { \"method\": \"POST\", \"path\": \"/domain/zone/*/record\" }, { \"method\": \"DELETE\", \"path\": \"/domain/zone/*/record/*\" }, { \"method\": \"POST\", \"path\": \"/domain/zone/*/refresh\" } ], \"redirection\":\"https://github.com/kubernetes-sigs/external-dns/blob/HEAD/docs/tutorials/ovh.md#creating-ovh-credentials\" }' Deploy ExternalDNS \u00b6 Connect your kubectl client to the cluster with which you want to test ExternalDNS, and then apply one of the following manifest files for deployment: Manifest (for clusters without RBAC enabled) \u00b6 apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=ovh env : - name : OVH_APPLICATION_KEY value : \"YOUR_OVH_APPLICATION_KEY\" - name : OVH_APPLICATION_SECRET value : \"YOUR_OVH_APPLICATION_SECRET\" - name : OVH_CONSUMER_KEY value : \"YOUR_OVH_CONSUMER_KEY_AFTER_VALIDATED_LINK\" Manifest (for clusters with RBAC enabled) \u00b6 apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" ] - apiGroups : [ \"\" ] resources : [ \"endpoints\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=ovh env : - name : OVH_APPLICATION_KEY value : \"YOUR_OVH_APPLICATION_KEY\" - name : OVH_APPLICATION_SECRET value : \"YOUR_OVH_APPLICATION_SECRET\" - name : OVH_CONSUMER_KEY value : \"YOUR_OVH_CONSUMER_KEY_AFTER_VALIDATED_LINK\" Deploying an Nginx Service \u00b6 Create a service file called \u2018nginx.yaml\u2019 with the following contents: apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 --- apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : example.com external-dns.alpha.kubernetes.io/ttl : \"120\" #optional spec : selector : app : nginx type : LoadBalancer ports : - protocol : TCP port : 80 targetPort : 80 A note about annotations Verify that the annotation on the service uses the same hostname as the OVH DNS zone created above. The annotation may also be a subdomain of the DNS zone (e.g. \u2018www.example.com\u2019). The TTL annotation can be used to configure the TTL on DNS records managed by ExternalDNS and is optional. If this annotation is not set, the TTL on records managed by ExternalDNS will default to 10. ExternalDNS uses the hostname annotation to determine which services should be registered with DNS. Removing the hostname annotation will cause ExternalDNS to remove the corresponding DNS records. Create the deployment and service \u00b6 $ kubectl create -f nginx.yaml Depending on where you run your service, it may take some time for your cloud provider to create an external IP for the service. Once an external IP is assigned, ExternalDNS detects the new service IP address and synchronizes the OVH DNS records. Verifying OVH DNS records \u00b6 Use the OVH manager or API to verify that the A record for your domain shows the external IP address of the services. Cleanup \u00b6 Once you successfully configure and verify record management via ExternalDNS, you can delete the tutorial\u2019s example: $ kubectl delete -f nginx.yaml $ kubectl delete -f externaldns.yaml","title":"Setting up ExternalDNS for Services on OVH"},{"location":"tutorials/ovh/#setting-up-externaldns-for-services-on-ovh","text":"This tutorial describes how to setup ExternalDNS for use within a Kubernetes cluster using OVH DNS. Make sure to use >=0.6 version of ExternalDNS for this tutorial.","title":"Setting up ExternalDNS for Services on OVH"},{"location":"tutorials/ovh/#creating-a-zone-with-ovh-dns","text":"If you are new to OVH, we recommend you first read the following instructions for creating a zone. Creating a zone using the OVH manager Creating a zone using the OVH API","title":"Creating a zone with OVH DNS"},{"location":"tutorials/ovh/#creating-ovh-credentials","text":"You first need to create an OVH application. Using the OVH documentation you will have your Application key and Application secret And you will need to generate your consumer key, here the permissions needed : - GET on /domain/zone - GET on /domain/zone/*/record - GET on /domain/zone/*/record/* - POST on /domain/zone/*/record - DELETE on /domain/zone/*/record/* - GET on /domain/zone/*/soa - POST on /domain/zone/*/refresh You can use the following curl request to generate & validated your Consumer key curl -XPOST -H \"X-Ovh-Application: \" -H \"Content-type: application/json\" https://eu.api.ovh.com/1.0/auth/credential -d '{ \"accessRules\": [ { \"method\": \"GET\", \"path\": \"/domain/zone\" }, { \"method\": \"GET\", \"path\": \"/domain/zone/*/soa\" }, { \"method\": \"GET\", \"path\": \"/domain/zone/*/record\" }, { \"method\": \"GET\", \"path\": \"/domain/zone/*/record/*\" }, { \"method\": \"POST\", \"path\": \"/domain/zone/*/record\" }, { \"method\": \"DELETE\", \"path\": \"/domain/zone/*/record/*\" }, { \"method\": \"POST\", \"path\": \"/domain/zone/*/refresh\" } ], \"redirection\":\"https://github.com/kubernetes-sigs/external-dns/blob/HEAD/docs/tutorials/ovh.md#creating-ovh-credentials\" }'","title":"Creating OVH Credentials"},{"location":"tutorials/ovh/#deploy-externaldns","text":"Connect your kubectl client to the cluster with which you want to test ExternalDNS, and then apply one of the following manifest files for deployment:","title":"Deploy ExternalDNS"},{"location":"tutorials/ovh/#manifest-for-clusters-without-rbac-enabled","text":"apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=ovh env : - name : OVH_APPLICATION_KEY value : \"YOUR_OVH_APPLICATION_KEY\" - name : OVH_APPLICATION_SECRET value : \"YOUR_OVH_APPLICATION_SECRET\" - name : OVH_CONSUMER_KEY value : \"YOUR_OVH_CONSUMER_KEY_AFTER_VALIDATED_LINK\"","title":"Manifest (for clusters without RBAC enabled)"},{"location":"tutorials/ovh/#manifest-for-clusters-with-rbac-enabled","text":"apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" ] - apiGroups : [ \"\" ] resources : [ \"endpoints\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=ovh env : - name : OVH_APPLICATION_KEY value : \"YOUR_OVH_APPLICATION_KEY\" - name : OVH_APPLICATION_SECRET value : \"YOUR_OVH_APPLICATION_SECRET\" - name : OVH_CONSUMER_KEY value : \"YOUR_OVH_CONSUMER_KEY_AFTER_VALIDATED_LINK\"","title":"Manifest (for clusters with RBAC enabled)"},{"location":"tutorials/ovh/#deploying-an-nginx-service","text":"Create a service file called \u2018nginx.yaml\u2019 with the following contents: apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 --- apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : example.com external-dns.alpha.kubernetes.io/ttl : \"120\" #optional spec : selector : app : nginx type : LoadBalancer ports : - protocol : TCP port : 80 targetPort : 80 A note about annotations Verify that the annotation on the service uses the same hostname as the OVH DNS zone created above. The annotation may also be a subdomain of the DNS zone (e.g. \u2018www.example.com\u2019). The TTL annotation can be used to configure the TTL on DNS records managed by ExternalDNS and is optional. If this annotation is not set, the TTL on records managed by ExternalDNS will default to 10. ExternalDNS uses the hostname annotation to determine which services should be registered with DNS. Removing the hostname annotation will cause ExternalDNS to remove the corresponding DNS records.","title":"Deploying an Nginx Service"},{"location":"tutorials/ovh/#create-the-deployment-and-service","text":"$ kubectl create -f nginx.yaml Depending on where you run your service, it may take some time for your cloud provider to create an external IP for the service. Once an external IP is assigned, ExternalDNS detects the new service IP address and synchronizes the OVH DNS records.","title":"Create the deployment and service"},{"location":"tutorials/ovh/#verifying-ovh-dns-records","text":"Use the OVH manager or API to verify that the A record for your domain shows the external IP address of the services.","title":"Verifying OVH DNS records"},{"location":"tutorials/ovh/#cleanup","text":"Once you successfully configure and verify record management via ExternalDNS, you can delete the tutorial\u2019s example: $ kubectl delete -f nginx.yaml $ kubectl delete -f externaldns.yaml","title":"Cleanup"},{"location":"tutorials/pdns/","text":"Setting up ExternalDNS for PowerDNS \u00b6 Prerequisites \u00b6 The provider has been written for and tested against PowerDNS v4.1.x and thus requires PowerDNS Auth Server >= 4.1.x PowerDNS provider support was added via this PR , thus you need to use external-dns version >= v0.5 The PDNS provider expects that your PowerDNS instance is already setup and functional. It expects that zones, you wish to add records to, already exist and are configured correctly. It does not add, remove or configure new zones in anyway. Feature Support \u00b6 The PDNS provider currently does not support: Dry running a configuration is not supported Deployment \u00b6 Deploying external DNS for PowerDNS is actually nearly identical to deploying it for other providers. This is what a sample deployment.yaml looks like: apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : # Only use if you're also using RBAC # serviceAccountName: external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # or ingress or both - --provider=pdns - --pdns-server={{ pdns-api-url }} - --pdns-api-key={{ pdns-http-api-key }} - --txt-owner-id={{ owner-id-for-this-external-dns }} - --domain-filter=external-dns-test.my-org.com # will make ExternalDNS see only the zones matching provided domain; omit to process all available zones in PowerDNS - --log-level=debug - --interval=30s Domain Filter ( --domain-filter ) \u00b6 When the --domain-filter argument is specified, external-dns will only create DNS records for host names (specified in ingress objects and services with the external-dns annotation) related to zones 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. The filter can also match parent zones. For example --domain-filter=a.example.com will allow for zone example.com . If you want to match parent zones, you cannot pre-pend your filter with a \u201c.\u201d, eg. --domain-filter=.example.com will not attempt to match parent zones. Regex Domain Filter ( --regex-domain-filter ) \u00b6 --regex-domain-filter limits possible domains and target zone with a regex. It overrides domain filters and can be specified only once. RBAC \u00b6 If your cluster is RBAC enabled, you also need to setup the following, before you can run external-dns: apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default Testing and Verification \u00b6 Important! : Remember to change example.com with your own domain throughout the following text. Spin up a simple \u201cHello World\u201d HTTP server with the following spec ( kubectl apply -f ): apiVersion : apps/v1 kind : Deployment metadata : name : echo spec : selector : matchLabels : app : echo template : metadata : labels : app : echo spec : containers : - image : hashicorp/http-echo name : echo ports : - containerPort : 5678 args : - -text=\"Hello World\" --- apiVersion : v1 kind : Service metadata : name : echo annotations : external-dns.alpha.kubernetes.io/hostname : echo.example.com spec : selector : app : echo type : LoadBalancer ports : - protocol : TCP port : 80 targetPort : 5678 Important! : Don\u2019t run dig, nslookup or similar immediately (until you\u2019ve confirmed the record exists). You\u2019ll get hit by negative DNS caching , which is hard to flush. Run the following to make sure everything is in order: $ kubectl get services echo $ kubectl get endpoints echo Make sure everything looks correct, i.e the service is defined and receives a public IP, and that the endpoint also has a pod IP. Once that\u2019s done, wait about 30s-1m (interval for external-dns to kick in), then do: $ curl -H \"X-API-Key: ${ PDNS_API_KEY } \" ${ PDNS_API_URL } /api/v1/servers/localhost/zones/example.com. | jq '.rrsets[] | select(.name | contains(\"echo\"))' Once the API shows the record correctly, you can double check your record using: $ dig @ ${ PDNS_FQDN } echo.example.com.","title":"Setting up ExternalDNS for PowerDNS"},{"location":"tutorials/pdns/#setting-up-externaldns-for-powerdns","text":"","title":"Setting up ExternalDNS for PowerDNS"},{"location":"tutorials/pdns/#prerequisites","text":"The provider has been written for and tested against PowerDNS v4.1.x and thus requires PowerDNS Auth Server >= 4.1.x PowerDNS provider support was added via this PR , thus you need to use external-dns version >= v0.5 The PDNS provider expects that your PowerDNS instance is already setup and functional. It expects that zones, you wish to add records to, already exist and are configured correctly. It does not add, remove or configure new zones in anyway.","title":"Prerequisites"},{"location":"tutorials/pdns/#feature-support","text":"The PDNS provider currently does not support: Dry running a configuration is not supported","title":"Feature Support"},{"location":"tutorials/pdns/#deployment","text":"Deploying external DNS for PowerDNS is actually nearly identical to deploying it for other providers. This is what a sample deployment.yaml looks like: apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : # Only use if you're also using RBAC # serviceAccountName: external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # or ingress or both - --provider=pdns - --pdns-server={{ pdns-api-url }} - --pdns-api-key={{ pdns-http-api-key }} - --txt-owner-id={{ owner-id-for-this-external-dns }} - --domain-filter=external-dns-test.my-org.com # will make ExternalDNS see only the zones matching provided domain; omit to process all available zones in PowerDNS - --log-level=debug - --interval=30s","title":"Deployment"},{"location":"tutorials/pdns/#domain-filter-domain-filter","text":"When the --domain-filter argument is specified, external-dns will only create DNS records for host names (specified in ingress objects and services with the external-dns annotation) related to zones 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. The filter can also match parent zones. For example --domain-filter=a.example.com will allow for zone example.com . If you want to match parent zones, you cannot pre-pend your filter with a \u201c.\u201d, eg. --domain-filter=.example.com will not attempt to match parent zones.","title":"Domain Filter (--domain-filter)"},{"location":"tutorials/pdns/#regex-domain-filter-regex-domain-filter","text":"--regex-domain-filter limits possible domains and target zone with a regex. It overrides domain filters and can be specified only once.","title":"Regex Domain Filter (--regex-domain-filter)"},{"location":"tutorials/pdns/#rbac","text":"If your cluster is RBAC enabled, you also need to setup the following, before you can run external-dns: apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default","title":"RBAC"},{"location":"tutorials/pdns/#testing-and-verification","text":"Important! : Remember to change example.com with your own domain throughout the following text. Spin up a simple \u201cHello World\u201d HTTP server with the following spec ( kubectl apply -f ): apiVersion : apps/v1 kind : Deployment metadata : name : echo spec : selector : matchLabels : app : echo template : metadata : labels : app : echo spec : containers : - image : hashicorp/http-echo name : echo ports : - containerPort : 5678 args : - -text=\"Hello World\" --- apiVersion : v1 kind : Service metadata : name : echo annotations : external-dns.alpha.kubernetes.io/hostname : echo.example.com spec : selector : app : echo type : LoadBalancer ports : - protocol : TCP port : 80 targetPort : 5678 Important! : Don\u2019t run dig, nslookup or similar immediately (until you\u2019ve confirmed the record exists). You\u2019ll get hit by negative DNS caching , which is hard to flush. Run the following to make sure everything is in order: $ kubectl get services echo $ kubectl get endpoints echo Make sure everything looks correct, i.e the service is defined and receives a public IP, and that the endpoint also has a pod IP. Once that\u2019s done, wait about 30s-1m (interval for external-dns to kick in), then do: $ curl -H \"X-API-Key: ${ PDNS_API_KEY } \" ${ PDNS_API_URL } /api/v1/servers/localhost/zones/example.com. | jq '.rrsets[] | select(.name | contains(\"echo\"))' Once the API shows the record correctly, you can double check your record using: $ dig @ ${ PDNS_FQDN } echo.example.com.","title":"Testing and Verification"},{"location":"tutorials/pihole/","text":"Setting up ExternalDNS for Pi-hole \u00b6 This tutorial describes how to setup ExternalDNS to sync records with Pi-hole\u2019s Custom DNS. Pi-hole has an internal list it checks last when resolving requests. This list can contain any number of arbitrary A or CNAME records. There is a pseudo-API exposed that ExternalDNS is able to use to manage these records. Deploy ExternalDNS \u00b6 You can skip to the manifest if authentication is disabled on your Pi-hole instance or you don\u2019t want to use secrets. If your Pi-hole server\u2019s admin dashboard is protected by a password, you\u2019ll likely want to create a secret first containing its value. This is optional since you do retain the option to pass it as a flag with --pihole-password . You can create the secret with: kubectl create secret generic pihole-password \\ --from-literal EXTERNAL_DNS_PIHOLE_PASSWORD = supersecret Replacing \u201csupersecret\u201d with the actual password to your Pi-hole server. ExternalDNS Manifest \u00b6 Apply the following manifest to deploy ExternalDNS, editing values for your environment accordingly. Be sure to change the namespace in the ClusterRoleBinding if you are using a namespace other than default . --- apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" , \"watch\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 # If authentication is disabled and/or you didn't create # a secret, you can remove this block. envFrom : - secretRef : # Change this if you gave the secret a different name name : pihole-password args : - --source=service - --source=ingress # Pihole only supports A/CNAME records so there is no mechanism to track ownership. # You don't need to set this flag, but if you leave it unset, you will receive warning # logs when ExternalDNS attempts to create TXT records. - --registry=noop # IMPORTANT: If you have records that you manage manually in Pi-hole, set # the policy to upsert-only so they do not get deleted. - --policy=upsert-only - --provider=pihole # Change this to the actual address of your Pi-hole web server - --pihole-server=http://pihole-web.pihole.svc.cluster.local securityContext : fsGroup : 65534 # For ExternalDNS to be able to read Kubernetes token files Arguments \u00b6 --pihole-server (env: EXTERNAL_DNS_PIHOLE_SERVER) - The address of the Pi-hole web server --pihole-password (env: EXTERNAL_DNS_PIHOLE_PASSWORD) - The password to the Pi-hole web server (if enabled) --pihole-tls-skip-verify (env: EXTERNAL_DNS_PIHOLE_TLS_SKIP_VERIFY) - Skip verification of any TLS certificates served by the Pi-hole web server. Verify ExternalDNS Works \u00b6 Ingress Example \u00b6 Create an Ingress resource. ExternalDNS will use the hostname specified in the Ingress object. apiVersion : networking.k8s.io/v1 kind : Ingress metadata : name : foo spec : ingressClassName : nginx rules : - host : foo.bar.com http : paths : - path : / pathType : Prefix backend : service : name : foo port : number : 80 Service Example \u00b6 The below sample application can be used to verify Services work. For services ExternalDNS will look for the annotation external-dns.alpha.kubernetes.io/hostname on the service and use the corresponding value. --- apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : nginx.external-dns-test.homelab.com spec : type : LoadBalancer ports : - port : 80 name : http targetPort : 80 selector : app : nginx --- apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 name : http You can then query your Pi-hole to see if the record was created. Change @192.168.100.2 to the actual address of your DNS server $ dig +short @192.168.100.2 nginx.external-dns-test.homelab.com 192 .168.100.129","title":"Setting up ExternalDNS for Pi-hole"},{"location":"tutorials/pihole/#setting-up-externaldns-for-pi-hole","text":"This tutorial describes how to setup ExternalDNS to sync records with Pi-hole\u2019s Custom DNS. Pi-hole has an internal list it checks last when resolving requests. This list can contain any number of arbitrary A or CNAME records. There is a pseudo-API exposed that ExternalDNS is able to use to manage these records.","title":"Setting up ExternalDNS for Pi-hole"},{"location":"tutorials/pihole/#deploy-externaldns","text":"You can skip to the manifest if authentication is disabled on your Pi-hole instance or you don\u2019t want to use secrets. If your Pi-hole server\u2019s admin dashboard is protected by a password, you\u2019ll likely want to create a secret first containing its value. This is optional since you do retain the option to pass it as a flag with --pihole-password . You can create the secret with: kubectl create secret generic pihole-password \\ --from-literal EXTERNAL_DNS_PIHOLE_PASSWORD = supersecret Replacing \u201csupersecret\u201d with the actual password to your Pi-hole server.","title":"Deploy ExternalDNS"},{"location":"tutorials/pihole/#externaldns-manifest","text":"Apply the following manifest to deploy ExternalDNS, editing values for your environment accordingly. Be sure to change the namespace in the ClusterRoleBinding if you are using a namespace other than default . --- apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" , \"watch\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 # If authentication is disabled and/or you didn't create # a secret, you can remove this block. envFrom : - secretRef : # Change this if you gave the secret a different name name : pihole-password args : - --source=service - --source=ingress # Pihole only supports A/CNAME records so there is no mechanism to track ownership. # You don't need to set this flag, but if you leave it unset, you will receive warning # logs when ExternalDNS attempts to create TXT records. - --registry=noop # IMPORTANT: If you have records that you manage manually in Pi-hole, set # the policy to upsert-only so they do not get deleted. - --policy=upsert-only - --provider=pihole # Change this to the actual address of your Pi-hole web server - --pihole-server=http://pihole-web.pihole.svc.cluster.local securityContext : fsGroup : 65534 # For ExternalDNS to be able to read Kubernetes token files","title":"ExternalDNS Manifest"},{"location":"tutorials/pihole/#arguments","text":"--pihole-server (env: EXTERNAL_DNS_PIHOLE_SERVER) - The address of the Pi-hole web server --pihole-password (env: EXTERNAL_DNS_PIHOLE_PASSWORD) - The password to the Pi-hole web server (if enabled) --pihole-tls-skip-verify (env: EXTERNAL_DNS_PIHOLE_TLS_SKIP_VERIFY) - Skip verification of any TLS certificates served by the Pi-hole web server.","title":"Arguments"},{"location":"tutorials/pihole/#verify-externaldns-works","text":"","title":"Verify ExternalDNS Works"},{"location":"tutorials/pihole/#ingress-example","text":"Create an Ingress resource. ExternalDNS will use the hostname specified in the Ingress object. apiVersion : networking.k8s.io/v1 kind : Ingress metadata : name : foo spec : ingressClassName : nginx rules : - host : foo.bar.com http : paths : - path : / pathType : Prefix backend : service : name : foo port : number : 80","title":"Ingress Example"},{"location":"tutorials/pihole/#service-example","text":"The below sample application can be used to verify Services work. For services ExternalDNS will look for the annotation external-dns.alpha.kubernetes.io/hostname on the service and use the corresponding value. --- apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : nginx.external-dns-test.homelab.com spec : type : LoadBalancer ports : - port : 80 name : http targetPort : 80 selector : app : nginx --- apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 name : http You can then query your Pi-hole to see if the record was created. Change @192.168.100.2 to the actual address of your DNS server $ dig +short @192.168.100.2 nginx.external-dns-test.homelab.com 192 .168.100.129","title":"Service Example"},{"location":"tutorials/plural/","text":"Setting up ExternalDNS for Services on Plural \u00b6 This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using Plural DNS. Make sure to use >=0.12.3 version of ExternalDNS for this tutorial. Creating Plural Credentials \u00b6 A secret containing the a Plural access token is needed for this provider. You can get a token for your user here . To create the secret you can run kubectl create secret generic plural-env --from-literal=PLURAL_ACCESS_TOKEN= . Deploy ExternalDNS \u00b6 Connect your kubectl client to the cluster you want to test ExternalDNS with. Then apply one of the following manifests file to deploy ExternalDNS. Manifest (for clusters without RBAC enabled) \u00b6 apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=plural - --plural-cluster=example-plural-cluster - --plural-provider=aws # gcp, azure, equinix and kind are also possible env : - name : PLURAL_ACCESS_TOKEN valueFrom : secretKeyRef : key : PLURAL_ACCESS_TOKEN name : plural-env - name : PLURAL_ENDPOINT # (optional) use an alternative endpoint for Plural; defaults to https://app.plural.sh value : https://app.plural.sh Manifest (for clusters with RBAC enabled) \u00b6 apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" , \"watch\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=plural - --plural-cluster=example-plural-cluster - --plural-provider=aws # gcp, azure, equinix and kind are also possible env : - name : PLURAL_ACCESS_TOKEN valueFrom : secretKeyRef : key : PLURAL_ACCESS_TOKEN name : plural-env - name : PLURAL_ENDPOINT # (optional) use an alternative endpoint for Plural; defaults to https://app.plural.sh value : https://app.plural.sh Deploying an Nginx Service \u00b6 Create a service file called \u2018nginx.yaml\u2019 with the following contents: apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 --- apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : example.com spec : selector : app : nginx type : LoadBalancer ports : - protocol : TCP port : 80 targetPort : 80 Note the annotation on the service; use the same hostname as the Plural DNS zone created above. The annotation may also be a subdomain of the DNS zone (e.g. \u2018www.example.com\u2019). By setting the TTL annotation on the service, you have to pass a valid TTL, which must be 120 or above. This annotation is optional, if you won\u2019t set it, it will be 1 (automatic) which is 300. ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records. Create the deployment and service: $ kubectl create -f nginx.yaml Depending where you run your service it can take a little while for your cloud provider to create an external IP for the service. Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize the Plural DNS records. Verifying Plural DNS records \u00b6 Check your Plural domain overview to view the domains associated with your Plural account. There you can view the records for each domain. The records should show the external IP address of the service as the A record for your domain. Cleanup \u00b6 Now that we have verified that ExternalDNS will automatically manage Plural DNS records, we can delete the tutorial\u2019s example: ``` $ kubectl delete -f nginx.yaml $ kubectl delete -f externaldns.yaml","title":"Setting up ExternalDNS for Services on Plural"},{"location":"tutorials/plural/#setting-up-externaldns-for-services-on-plural","text":"This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using Plural DNS. Make sure to use >=0.12.3 version of ExternalDNS for this tutorial.","title":"Setting up ExternalDNS for Services on Plural"},{"location":"tutorials/plural/#creating-plural-credentials","text":"A secret containing the a Plural access token is needed for this provider. You can get a token for your user here . To create the secret you can run kubectl create secret generic plural-env --from-literal=PLURAL_ACCESS_TOKEN= .","title":"Creating Plural Credentials"},{"location":"tutorials/plural/#deploy-externaldns","text":"Connect your kubectl client to the cluster you want to test ExternalDNS with. Then apply one of the following manifests file to deploy ExternalDNS.","title":"Deploy ExternalDNS"},{"location":"tutorials/plural/#manifest-for-clusters-without-rbac-enabled","text":"apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=plural - --plural-cluster=example-plural-cluster - --plural-provider=aws # gcp, azure, equinix and kind are also possible env : - name : PLURAL_ACCESS_TOKEN valueFrom : secretKeyRef : key : PLURAL_ACCESS_TOKEN name : plural-env - name : PLURAL_ENDPOINT # (optional) use an alternative endpoint for Plural; defaults to https://app.plural.sh value : https://app.plural.sh","title":"Manifest (for clusters without RBAC enabled)"},{"location":"tutorials/plural/#manifest-for-clusters-with-rbac-enabled","text":"apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" , \"watch\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=plural - --plural-cluster=example-plural-cluster - --plural-provider=aws # gcp, azure, equinix and kind are also possible env : - name : PLURAL_ACCESS_TOKEN valueFrom : secretKeyRef : key : PLURAL_ACCESS_TOKEN name : plural-env - name : PLURAL_ENDPOINT # (optional) use an alternative endpoint for Plural; defaults to https://app.plural.sh value : https://app.plural.sh","title":"Manifest (for clusters with RBAC enabled)"},{"location":"tutorials/plural/#deploying-an-nginx-service","text":"Create a service file called \u2018nginx.yaml\u2019 with the following contents: apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 --- apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : example.com spec : selector : app : nginx type : LoadBalancer ports : - protocol : TCP port : 80 targetPort : 80 Note the annotation on the service; use the same hostname as the Plural DNS zone created above. The annotation may also be a subdomain of the DNS zone (e.g. \u2018www.example.com\u2019). By setting the TTL annotation on the service, you have to pass a valid TTL, which must be 120 or above. This annotation is optional, if you won\u2019t set it, it will be 1 (automatic) which is 300. ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records. Create the deployment and service: $ kubectl create -f nginx.yaml Depending where you run your service it can take a little while for your cloud provider to create an external IP for the service. Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize the Plural DNS records.","title":"Deploying an Nginx Service"},{"location":"tutorials/plural/#verifying-plural-dns-records","text":"Check your Plural domain overview to view the domains associated with your Plural account. There you can view the records for each domain. The records should show the external IP address of the service as the A record for your domain.","title":"Verifying Plural DNS records"},{"location":"tutorials/plural/#cleanup","text":"Now that we have verified that ExternalDNS will automatically manage Plural DNS records, we can delete the tutorial\u2019s example: ``` $ kubectl delete -f nginx.yaml $ kubectl delete -f externaldns.yaml","title":"Cleanup"},{"location":"tutorials/public-private-route53/","text":"Setting up ExternalDNS using the same domain for public and private Route53 zones \u00b6 This tutorial describes how to setup ExternalDNS using the same domain for public and private Route53 zones and nginx-ingress-controller . It also outlines how to use cert-manager to automatically issue SSL certificates from Let\u2019s Encrypt for both public and private records. Deploy public nginx-ingress-controller \u00b6 Consult External DNS nginx ingress docs for installation guidelines. Specify ingress-class in nginx-ingress-controller container args: apiVersion : apps/v1 kind : Deployment metadata : labels : app : external-ingress name : external-ingress-controller spec : replicas : 1 selector : matchLabels : app : external-ingress template : metadata : labels : app : external-ingress spec : containers : - args : - /nginx-ingress-controller - --default-backend-service=$(POD_NAMESPACE)/default-http-backend - --configmap=$(POD_NAMESPACE)/external-ingress-configuration - --tcp-services-configmap=$(POD_NAMESPACE)/external-tcp-services - --udp-services-configmap=$(POD_NAMESPACE)/external-udp-services - --annotations-prefix=nginx.ingress.kubernetes.io - --ingress-class=external-ingress - --publish-service=$(POD_NAMESPACE)/external-ingress env : - name : POD_NAME valueFrom : fieldRef : apiVersion : v1 fieldPath : metadata.name - name : POD_NAMESPACE valueFrom : fieldRef : apiVersion : v1 fieldPath : metadata.namespace image : quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.11.0 livenessProbe : failureThreshold : 3 httpGet : path : /healthz port : 10254 scheme : HTTP initialDelaySeconds : 10 periodSeconds : 10 successThreshold : 1 timeoutSeconds : 1 name : external-ingress-controller ports : - containerPort : 80 name : http protocol : TCP - containerPort : 443 name : https protocol : TCP readinessProbe : failureThreshold : 3 httpGet : path : /healthz port : 10254 scheme : HTTP periodSeconds : 10 successThreshold : 1 timeoutSeconds : 1 Set type: LoadBalancer in your public nginx-ingress-controller Service definition. apiVersion : v1 kind : Service metadata : annotations : service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout : \"3600\" service.beta.kubernetes.io/aws-load-balancer-proxy-protocol : '*' labels : app : external-ingress name : external-ingress spec : externalTrafficPolicy : Cluster ports : - name : http port : 80 protocol : TCP targetPort : http - name : https port : 443 protocol : TCP targetPort : https selector : app : external-ingress sessionAffinity : None type : LoadBalancer Deploy private nginx-ingress-controller \u00b6 Consult External DNS nginx ingress docs for installation guidelines. Make sure to specify ingress-class in nginx-ingress-controller container args: apiVersion : apps/v1 kind : Deployment metadata : labels : app : internal-ingress name : internal-ingress-controller spec : replicas : 1 selector : matchLabels : app : internal-ingress template : metadata : labels : app : internal-ingress spec : containers : - args : - /nginx-ingress-controller - --default-backend-service=$(POD_NAMESPACE)/default-http-backend - --configmap=$(POD_NAMESPACE)/internal-ingress-configuration - --tcp-services-configmap=$(POD_NAMESPACE)/internal-tcp-services - --udp-services-configmap=$(POD_NAMESPACE)/internal-udp-services - --annotations-prefix=nginx.ingress.kubernetes.io - --ingress-class=internal-ingress - --publish-service=$(POD_NAMESPACE)/internal-ingress env : - name : POD_NAME valueFrom : fieldRef : apiVersion : v1 fieldPath : metadata.name - name : POD_NAMESPACE valueFrom : fieldRef : apiVersion : v1 fieldPath : metadata.namespace image : quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.11.0 livenessProbe : failureThreshold : 3 httpGet : path : /healthz port : 10254 scheme : HTTP initialDelaySeconds : 10 periodSeconds : 10 successThreshold : 1 timeoutSeconds : 1 name : internal-ingress-controller ports : - containerPort : 80 name : http protocol : TCP - containerPort : 443 name : https protocol : TCP readinessProbe : failureThreshold : 3 httpGet : path : /healthz port : 10254 scheme : HTTP periodSeconds : 10 successThreshold : 1 timeoutSeconds : 1 Set additional annotations in your private nginx-ingress-controller Service definition to create an internal load balancer. apiVersion : v1 kind : Service metadata : annotations : service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout : \"3600\" service.beta.kubernetes.io/aws-load-balancer-internal : 0.0.0.0/0 service.beta.kubernetes.io/aws-load-balancer-proxy-protocol : '*' labels : app : internal-ingress name : internal-ingress spec : externalTrafficPolicy : Cluster ports : - name : http port : 80 protocol : TCP targetPort : http - name : https port : 443 protocol : TCP targetPort : https selector : app : internal-ingress sessionAffinity : None type : LoadBalancer Deploy the public zone ExternalDNS \u00b6 Consult AWS ExternalDNS setup docs for installation guidelines. In ExternalDNS containers args, make sure to specify aws-zone-type and ingress-class : apiVersion : apps/v1 kind : Deployment metadata : labels : app : external-dns-public name : external-dns-public namespace : kube-system spec : replicas : 1 selector : matchLabels : app : external-dns-public strategy : type : Recreate template : metadata : labels : app : external-dns-public spec : containers : - args : - --source=ingress - --provider=aws - --registry=txt - --txt-owner-id=external-dns - --ingress-class=external-ingress - --aws-zone-type=public image : registry.k8s.io/external-dns/external-dns:v0.13.5 name : external-dns-public Deploy the private zone ExternalDNS \u00b6 Consult AWS ExternalDNS setup docs for installation guidelines. In ExternalDNS containers args, make sure to specify aws-zone-type and ingress-class : apiVersion : apps/v1 kind : Deployment metadata : labels : app : external-dns-private name : external-dns-private namespace : kube-system spec : replicas : 1 selector : matchLabels : app : external-dns-private strategy : type : Recreate template : metadata : labels : app : external-dns-private spec : containers : - args : - --source=ingress - --provider=aws - --registry=txt - --txt-owner-id=dev.k8s.nexus - --ingress-class=internal-ingress - --aws-zone-type=private image : registry.k8s.io/external-dns/external-dns:v0.13.5 name : external-dns-private Create application Service definitions \u00b6 For this setup to work, you need to create two Ingress definitions for your application. At first, create a public Ingress definition: apiVersion : networking.k8s.io/v1 kind : Ingress metadata : labels : app : app name : app-public spec : ingressClassName : external-ingress rules : - host : app.domain.com http : paths : - backend : service : name : app port : number : 80 pathType : Prefix Then create a private Ingress definition: apiVersion : networking.k8s.io/v1 kind : Ingress metadata : labels : app : app name : app-private spec : ingressClassName : internal-ingress rules : - host : app.domain.com http : paths : - backend : service : name : app port : number : 80 pathType : Prefix Additionally, you may leverage cert-manager to automatically issue SSL certificates from Let\u2019s Encrypt . To do that, request a certificate in public service definition: apiVersion : networking.k8s.io/v1 kind : Ingress metadata : annotations : certmanager.k8s.io/acme-challenge-type : \"dns01\" certmanager.k8s.io/acme-dns01-provider : \"route53\" certmanager.k8s.io/cluster-issuer : \"letsencrypt-production\" kubernetes.io/tls-acme : \"true\" labels : app : app name : app-public spec : ingressClassName : \"external-ingress\" rules : - host : app.domain.com http : paths : - backend : service : name : app port : number : 80 pathType : Prefix tls : - hosts : - app.domain.com secretName : app-tls And reuse the requested certificate in private Service definition: apiVersion : networking.k8s.io/v1 kind : Ingress metadata : labels : app : app name : app-private spec : ingressClassName : \"internal-ingress\" rules : - host : app.domain.com http : paths : - backend : service : name : app port : number : 80 pathType : Prefix tls : - hosts : - app.domain.com secretName : app-tls","title":"Setting up ExternalDNS using the same domain for public and private Route53 zones"},{"location":"tutorials/public-private-route53/#setting-up-externaldns-using-the-same-domain-for-public-and-private-route53-zones","text":"This tutorial describes how to setup ExternalDNS using the same domain for public and private Route53 zones and nginx-ingress-controller . It also outlines how to use cert-manager to automatically issue SSL certificates from Let\u2019s Encrypt for both public and private records.","title":"Setting up ExternalDNS using the same domain for public and private Route53 zones"},{"location":"tutorials/public-private-route53/#deploy-public-nginx-ingress-controller","text":"Consult External DNS nginx ingress docs for installation guidelines. Specify ingress-class in nginx-ingress-controller container args: apiVersion : apps/v1 kind : Deployment metadata : labels : app : external-ingress name : external-ingress-controller spec : replicas : 1 selector : matchLabels : app : external-ingress template : metadata : labels : app : external-ingress spec : containers : - args : - /nginx-ingress-controller - --default-backend-service=$(POD_NAMESPACE)/default-http-backend - --configmap=$(POD_NAMESPACE)/external-ingress-configuration - --tcp-services-configmap=$(POD_NAMESPACE)/external-tcp-services - --udp-services-configmap=$(POD_NAMESPACE)/external-udp-services - --annotations-prefix=nginx.ingress.kubernetes.io - --ingress-class=external-ingress - --publish-service=$(POD_NAMESPACE)/external-ingress env : - name : POD_NAME valueFrom : fieldRef : apiVersion : v1 fieldPath : metadata.name - name : POD_NAMESPACE valueFrom : fieldRef : apiVersion : v1 fieldPath : metadata.namespace image : quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.11.0 livenessProbe : failureThreshold : 3 httpGet : path : /healthz port : 10254 scheme : HTTP initialDelaySeconds : 10 periodSeconds : 10 successThreshold : 1 timeoutSeconds : 1 name : external-ingress-controller ports : - containerPort : 80 name : http protocol : TCP - containerPort : 443 name : https protocol : TCP readinessProbe : failureThreshold : 3 httpGet : path : /healthz port : 10254 scheme : HTTP periodSeconds : 10 successThreshold : 1 timeoutSeconds : 1 Set type: LoadBalancer in your public nginx-ingress-controller Service definition. apiVersion : v1 kind : Service metadata : annotations : service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout : \"3600\" service.beta.kubernetes.io/aws-load-balancer-proxy-protocol : '*' labels : app : external-ingress name : external-ingress spec : externalTrafficPolicy : Cluster ports : - name : http port : 80 protocol : TCP targetPort : http - name : https port : 443 protocol : TCP targetPort : https selector : app : external-ingress sessionAffinity : None type : LoadBalancer","title":"Deploy public nginx-ingress-controller"},{"location":"tutorials/public-private-route53/#deploy-private-nginx-ingress-controller","text":"Consult External DNS nginx ingress docs for installation guidelines. Make sure to specify ingress-class in nginx-ingress-controller container args: apiVersion : apps/v1 kind : Deployment metadata : labels : app : internal-ingress name : internal-ingress-controller spec : replicas : 1 selector : matchLabels : app : internal-ingress template : metadata : labels : app : internal-ingress spec : containers : - args : - /nginx-ingress-controller - --default-backend-service=$(POD_NAMESPACE)/default-http-backend - --configmap=$(POD_NAMESPACE)/internal-ingress-configuration - --tcp-services-configmap=$(POD_NAMESPACE)/internal-tcp-services - --udp-services-configmap=$(POD_NAMESPACE)/internal-udp-services - --annotations-prefix=nginx.ingress.kubernetes.io - --ingress-class=internal-ingress - --publish-service=$(POD_NAMESPACE)/internal-ingress env : - name : POD_NAME valueFrom : fieldRef : apiVersion : v1 fieldPath : metadata.name - name : POD_NAMESPACE valueFrom : fieldRef : apiVersion : v1 fieldPath : metadata.namespace image : quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.11.0 livenessProbe : failureThreshold : 3 httpGet : path : /healthz port : 10254 scheme : HTTP initialDelaySeconds : 10 periodSeconds : 10 successThreshold : 1 timeoutSeconds : 1 name : internal-ingress-controller ports : - containerPort : 80 name : http protocol : TCP - containerPort : 443 name : https protocol : TCP readinessProbe : failureThreshold : 3 httpGet : path : /healthz port : 10254 scheme : HTTP periodSeconds : 10 successThreshold : 1 timeoutSeconds : 1 Set additional annotations in your private nginx-ingress-controller Service definition to create an internal load balancer. apiVersion : v1 kind : Service metadata : annotations : service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout : \"3600\" service.beta.kubernetes.io/aws-load-balancer-internal : 0.0.0.0/0 service.beta.kubernetes.io/aws-load-balancer-proxy-protocol : '*' labels : app : internal-ingress name : internal-ingress spec : externalTrafficPolicy : Cluster ports : - name : http port : 80 protocol : TCP targetPort : http - name : https port : 443 protocol : TCP targetPort : https selector : app : internal-ingress sessionAffinity : None type : LoadBalancer","title":"Deploy private nginx-ingress-controller"},{"location":"tutorials/public-private-route53/#deploy-the-public-zone-externaldns","text":"Consult AWS ExternalDNS setup docs for installation guidelines. In ExternalDNS containers args, make sure to specify aws-zone-type and ingress-class : apiVersion : apps/v1 kind : Deployment metadata : labels : app : external-dns-public name : external-dns-public namespace : kube-system spec : replicas : 1 selector : matchLabels : app : external-dns-public strategy : type : Recreate template : metadata : labels : app : external-dns-public spec : containers : - args : - --source=ingress - --provider=aws - --registry=txt - --txt-owner-id=external-dns - --ingress-class=external-ingress - --aws-zone-type=public image : registry.k8s.io/external-dns/external-dns:v0.13.5 name : external-dns-public","title":"Deploy the public zone ExternalDNS"},{"location":"tutorials/public-private-route53/#deploy-the-private-zone-externaldns","text":"Consult AWS ExternalDNS setup docs for installation guidelines. In ExternalDNS containers args, make sure to specify aws-zone-type and ingress-class : apiVersion : apps/v1 kind : Deployment metadata : labels : app : external-dns-private name : external-dns-private namespace : kube-system spec : replicas : 1 selector : matchLabels : app : external-dns-private strategy : type : Recreate template : metadata : labels : app : external-dns-private spec : containers : - args : - --source=ingress - --provider=aws - --registry=txt - --txt-owner-id=dev.k8s.nexus - --ingress-class=internal-ingress - --aws-zone-type=private image : registry.k8s.io/external-dns/external-dns:v0.13.5 name : external-dns-private","title":"Deploy the private zone ExternalDNS"},{"location":"tutorials/public-private-route53/#create-application-service-definitions","text":"For this setup to work, you need to create two Ingress definitions for your application. At first, create a public Ingress definition: apiVersion : networking.k8s.io/v1 kind : Ingress metadata : labels : app : app name : app-public spec : ingressClassName : external-ingress rules : - host : app.domain.com http : paths : - backend : service : name : app port : number : 80 pathType : Prefix Then create a private Ingress definition: apiVersion : networking.k8s.io/v1 kind : Ingress metadata : labels : app : app name : app-private spec : ingressClassName : internal-ingress rules : - host : app.domain.com http : paths : - backend : service : name : app port : number : 80 pathType : Prefix Additionally, you may leverage cert-manager to automatically issue SSL certificates from Let\u2019s Encrypt . To do that, request a certificate in public service definition: apiVersion : networking.k8s.io/v1 kind : Ingress metadata : annotations : certmanager.k8s.io/acme-challenge-type : \"dns01\" certmanager.k8s.io/acme-dns01-provider : \"route53\" certmanager.k8s.io/cluster-issuer : \"letsencrypt-production\" kubernetes.io/tls-acme : \"true\" labels : app : app name : app-public spec : ingressClassName : \"external-ingress\" rules : - host : app.domain.com http : paths : - backend : service : name : app port : number : 80 pathType : Prefix tls : - hosts : - app.domain.com secretName : app-tls And reuse the requested certificate in private Service definition: apiVersion : networking.k8s.io/v1 kind : Ingress metadata : labels : app : app name : app-private spec : ingressClassName : \"internal-ingress\" rules : - host : app.domain.com http : paths : - backend : service : name : app port : number : 80 pathType : Prefix tls : - hosts : - app.domain.com secretName : app-tls","title":"Create application Service definitions"},{"location":"tutorials/rcodezero/","text":"Setting up ExternalDNS for Services on RcodeZero \u00b6 This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using RcodeZero Anycast DNS . Make sure to use >=0.5.0 version of ExternalDNS for this tutorial. The following steps are required to use RcodeZero with ExternalDNS: Sign up for an RcodeZero account (or use an existing account). Add your zone to the RcodeZero DNS Enable the RcodeZero API, and generate an API key. Deploy ExternalDNS to use the RcodeZero provider. Verify the setup bey deploying a test services (optional) Creating a RcodeZero DNS zone \u00b6 Before records can be added to your domain name automatically, you need to add your domain name to the set of zones managed by RcodeZero. In order to add the zone, perform the following steps: Log in to the RcodeZero Dashboard, and move to the Add Zone page. Select \u201cMASTER\u201d as domain type, and add your domain name there. Use this domain name instead of \u201cexample.com\u201d throughout the rest of this tutorial. Note that \u201cSECONDARY\u201d domains cannot be managed by ExternalDNS, because this would not allow modification of records in the zone. Enable the API, and create Credentials \u00b6 The RcodeZero Anycast-Network is provisioned via web interface or REST-API. Enable the RcodeZero API to generate an API key on RcodeZero API . The API key will be added to the environment variable \u2018RC0_API_KEY\u2019 via one of the Manifest templates (as described below). Deploy ExternalDNS \u00b6 Connect your kubectl client to the cluster you want to test ExternalDNS with. Choose a Manifest from below, depending on whether or not you have RBAC enabled. Before applying it, modify the Manifest as follows: Replace \u201cexample.com\u201d with the domain name you added to RcodeZero. Replace YOUR_RCODEZERO_API_KEY with the API key created above. Replace YOUR_ENCRYPTION_KEY_STRING with a string to encrypt the TXT records Manifest (for clusters without RBAC enabled) \u00b6 apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=rcodezero - --rc0-enc-txt # (optional) encrypt TXT records; encryption key has to be provided with RC0_ENC_KEY env var. env : - name : RC0_API_KEY value : \"YOUR_RCODEZERO_API_KEY\" - name : RC0_ENC_VAR value : \"YOUR_ENCRYPTION_KEY_STRING\" Manifest (for clusters with RBAC enabled) \u00b6 apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=rcodezero - --rc0-enc-txt # (optional) encrypt TXT records; encryption key has to be provided with RC0_ENC_KEY env var. env : - name : RC0_API_KEY value : \"YOUR_RCODEZERO_API_KEY\" - name : RC0_ENC_VAR value : \"YOUR_ENCRYPTION_KEY_STRING\" Deploying an Nginx Service \u00b6 After you have deployed ExternalDNS with RcodeZero, you can deploy a simple service based on Nginx to test the setup. This is optional, though highly recommended before using ExternalDNS in production. Create a service file called \u2018nginx.yaml\u2019 with the following contents: apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 --- apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : example.com external-dns.alpha.kubernetes.io/ttl : \"120\" #optional spec : selector : app : nginx type : LoadBalancer ports : - protocol : TCP port : 80 targetPort : 80 Change the file as follows: Replace the annotation of the service; use the same hostname as the RcodeZero DNS zone created above. The annotation may also be a subdomain of the DNS zone (e.g. \u2018www.example.com\u2019). Set the TTL annotation of the service. A valid TTL of 120 or above must be given. This annotation is optional, and defaults to \u201c300\u201d if no value is given. These annotations will be used to determine what services should be registered with DNS. Removing these annotations will cause ExternalDNS to remove the corresponding DNS records. Create the Deployment and Service: $ kubectl create -f nginx.yaml Depending on your cloud provider, it might take a while to create an external IP for the service. Once an external IP address is assigned to the service, ExternalDNS will notice the new address and synchronize the RcodeZero DNS records accordingly. Verifying RcodeZero DNS records \u00b6 Check your RcodeZero Configured Zones and select the respective zone name. The zone should now contain the external IP address of the service as an A record. Cleanup \u00b6 Once you have verified that ExternalDNS successfully manages RcodeZero DNS records for external services, you can delete the tutorial example as follows: $ kubectl delete -f nginx.yaml $ kubectl delete -f externaldns.yaml","title":"Setting up ExternalDNS for Services on RcodeZero"},{"location":"tutorials/rcodezero/#setting-up-externaldns-for-services-on-rcodezero","text":"This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using RcodeZero Anycast DNS . Make sure to use >=0.5.0 version of ExternalDNS for this tutorial. The following steps are required to use RcodeZero with ExternalDNS: Sign up for an RcodeZero account (or use an existing account). Add your zone to the RcodeZero DNS Enable the RcodeZero API, and generate an API key. Deploy ExternalDNS to use the RcodeZero provider. Verify the setup bey deploying a test services (optional)","title":"Setting up ExternalDNS for Services on RcodeZero"},{"location":"tutorials/rcodezero/#creating-a-rcodezero-dns-zone","text":"Before records can be added to your domain name automatically, you need to add your domain name to the set of zones managed by RcodeZero. In order to add the zone, perform the following steps: Log in to the RcodeZero Dashboard, and move to the Add Zone page. Select \u201cMASTER\u201d as domain type, and add your domain name there. Use this domain name instead of \u201cexample.com\u201d throughout the rest of this tutorial. Note that \u201cSECONDARY\u201d domains cannot be managed by ExternalDNS, because this would not allow modification of records in the zone.","title":"Creating a RcodeZero DNS zone"},{"location":"tutorials/rcodezero/#enable-the-api-and-create-credentials","text":"The RcodeZero Anycast-Network is provisioned via web interface or REST-API. Enable the RcodeZero API to generate an API key on RcodeZero API . The API key will be added to the environment variable \u2018RC0_API_KEY\u2019 via one of the Manifest templates (as described below).","title":"Enable the API, and create Credentials"},{"location":"tutorials/rcodezero/#deploy-externaldns","text":"Connect your kubectl client to the cluster you want to test ExternalDNS with. Choose a Manifest from below, depending on whether or not you have RBAC enabled. Before applying it, modify the Manifest as follows: Replace \u201cexample.com\u201d with the domain name you added to RcodeZero. Replace YOUR_RCODEZERO_API_KEY with the API key created above. Replace YOUR_ENCRYPTION_KEY_STRING with a string to encrypt the TXT records","title":"Deploy ExternalDNS"},{"location":"tutorials/rcodezero/#manifest-for-clusters-without-rbac-enabled","text":"apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=rcodezero - --rc0-enc-txt # (optional) encrypt TXT records; encryption key has to be provided with RC0_ENC_KEY env var. env : - name : RC0_API_KEY value : \"YOUR_RCODEZERO_API_KEY\" - name : RC0_ENC_VAR value : \"YOUR_ENCRYPTION_KEY_STRING\"","title":"Manifest (for clusters without RBAC enabled)"},{"location":"tutorials/rcodezero/#manifest-for-clusters-with-rbac-enabled","text":"apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=rcodezero - --rc0-enc-txt # (optional) encrypt TXT records; encryption key has to be provided with RC0_ENC_KEY env var. env : - name : RC0_API_KEY value : \"YOUR_RCODEZERO_API_KEY\" - name : RC0_ENC_VAR value : \"YOUR_ENCRYPTION_KEY_STRING\"","title":"Manifest (for clusters with RBAC enabled)"},{"location":"tutorials/rcodezero/#deploying-an-nginx-service","text":"After you have deployed ExternalDNS with RcodeZero, you can deploy a simple service based on Nginx to test the setup. This is optional, though highly recommended before using ExternalDNS in production. Create a service file called \u2018nginx.yaml\u2019 with the following contents: apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 --- apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : example.com external-dns.alpha.kubernetes.io/ttl : \"120\" #optional spec : selector : app : nginx type : LoadBalancer ports : - protocol : TCP port : 80 targetPort : 80 Change the file as follows: Replace the annotation of the service; use the same hostname as the RcodeZero DNS zone created above. The annotation may also be a subdomain of the DNS zone (e.g. \u2018www.example.com\u2019). Set the TTL annotation of the service. A valid TTL of 120 or above must be given. This annotation is optional, and defaults to \u201c300\u201d if no value is given. These annotations will be used to determine what services should be registered with DNS. Removing these annotations will cause ExternalDNS to remove the corresponding DNS records. Create the Deployment and Service: $ kubectl create -f nginx.yaml Depending on your cloud provider, it might take a while to create an external IP for the service. Once an external IP address is assigned to the service, ExternalDNS will notice the new address and synchronize the RcodeZero DNS records accordingly.","title":"Deploying an Nginx Service"},{"location":"tutorials/rcodezero/#verifying-rcodezero-dns-records","text":"Check your RcodeZero Configured Zones and select the respective zone name. The zone should now contain the external IP address of the service as an A record.","title":"Verifying RcodeZero DNS records"},{"location":"tutorials/rcodezero/#cleanup","text":"Once you have verified that ExternalDNS successfully manages RcodeZero DNS records for external services, you can delete the tutorial example as follows: $ kubectl delete -f nginx.yaml $ kubectl delete -f externaldns.yaml","title":"Cleanup"},{"location":"tutorials/rdns/","text":"Setting up ExternalDNS for RancherDNS(RDNS) with kubernetes \u00b6 This tutorial describes how to setup ExternalDNS for usage within a kubernetes cluster that makes use of RDNS and nginx ingress controller . You need to: * install RDNS with etcd enabled * install external-dns with rdns as a provider Installing RDNS with etcdv3 backend \u00b6 Clone RDNS \u00b6 git clone https://github.com/rancher/rdns-server.git Installing ETCD \u00b6 cd rdns-server docker-compose -f deploy/etcdv3/etcd-compose.yaml up -d ETCD was successfully deployed on http://172.31.35.77:2379 Installing RDNS \u00b6 export ETCD_ENDPOINTS=\"http://172.31.35.77:2379\" export DOMAIN=\"lb.rancher.cloud\" ./scripts/start etcdv3 RDNS was successfully deployed on 172.31.35.77 Installing ExternalDNS \u00b6 Install external ExternalDNS \u00b6 ETCD_URLS is configured to etcd client service address. RDNS_ROOT_DOMAIN is configured to the same with RDNS DOMAIN environment. e.g. lb.rancher.cloud. Manifest (for clusters without RBAC enabled) \u00b6 apiVersion : apps/v1 kind : Deployment metadata : name : external-dns namespace : kube-system spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=ingress - --provider=rdns - --log-level=debug # debug only env : - name : ETCD_URLS value : http://172.31.35.77:2379 - name : RDNS_ROOT_DOMAIN value : lb.rancher.cloud Manifest (for clusters with RBAC enabled) \u00b6 --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : kube-system --- apiVersion : v1 kind : ServiceAccount metadata : name : external-dns namespace : kube-system --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns namespace : kube-system spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=ingress - --provider=rdns - --log-level=debug # debug only env : - name : ETCD_URLS value : http://172.31.35.77:2379 - name : RDNS_ROOT_DOMAIN value : lb.rancher.cloud Testing ingress example \u00b6 $ cat ingress.yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: nginx spec: ingressClassName: nginx rules: - host: nginx.lb.rancher.cloud http: paths: - backend: serviceName: nginx servicePort: 80 $ kubectl apply -f ingress.yaml ingress.extensions \"nginx\" created Wait a moment until DNS has the ingress IP. The RDNS IP in this example is \u201c172.31.35.77\u201d. $ kubectl get ingress NAME HOSTS ADDRESS PORTS AGE nginx nginx.lb.rancher.cloud 172.31.42.211 80 2m $ kubectl run -it --rm --restart=Never --image=infoblox/dnstools:latest dnstools If you don't see a command prompt, try pressing enter. dnstools# dig @172.31.35.77 nginx.lb.rancher.cloud +short 172.31.42.211 dnstools#","title":"Setting up ExternalDNS for RancherDNS(RDNS) with kubernetes"},{"location":"tutorials/rdns/#setting-up-externaldns-for-rancherdnsrdns-with-kubernetes","text":"This tutorial describes how to setup ExternalDNS for usage within a kubernetes cluster that makes use of RDNS and nginx ingress controller . You need to: * install RDNS with etcd enabled * install external-dns with rdns as a provider","title":"Setting up ExternalDNS for RancherDNS(RDNS) with kubernetes"},{"location":"tutorials/rdns/#installing-rdns-with-etcdv3-backend","text":"","title":"Installing RDNS with etcdv3 backend"},{"location":"tutorials/rdns/#clone-rdns","text":"git clone https://github.com/rancher/rdns-server.git","title":"Clone RDNS"},{"location":"tutorials/rdns/#installing-etcd","text":"cd rdns-server docker-compose -f deploy/etcdv3/etcd-compose.yaml up -d ETCD was successfully deployed on http://172.31.35.77:2379","title":"Installing ETCD"},{"location":"tutorials/rdns/#installing-rdns","text":"export ETCD_ENDPOINTS=\"http://172.31.35.77:2379\" export DOMAIN=\"lb.rancher.cloud\" ./scripts/start etcdv3 RDNS was successfully deployed on 172.31.35.77","title":"Installing RDNS"},{"location":"tutorials/rdns/#installing-externaldns","text":"","title":"Installing ExternalDNS"},{"location":"tutorials/rdns/#install-external-externaldns","text":"ETCD_URLS is configured to etcd client service address. RDNS_ROOT_DOMAIN is configured to the same with RDNS DOMAIN environment. e.g. lb.rancher.cloud.","title":"Install external ExternalDNS"},{"location":"tutorials/rdns/#manifest-for-clusters-without-rbac-enabled","text":"apiVersion : apps/v1 kind : Deployment metadata : name : external-dns namespace : kube-system spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=ingress - --provider=rdns - --log-level=debug # debug only env : - name : ETCD_URLS value : http://172.31.35.77:2379 - name : RDNS_ROOT_DOMAIN value : lb.rancher.cloud","title":"Manifest (for clusters without RBAC enabled)"},{"location":"tutorials/rdns/#manifest-for-clusters-with-rbac-enabled","text":"--- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : kube-system --- apiVersion : v1 kind : ServiceAccount metadata : name : external-dns namespace : kube-system --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns namespace : kube-system spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=ingress - --provider=rdns - --log-level=debug # debug only env : - name : ETCD_URLS value : http://172.31.35.77:2379 - name : RDNS_ROOT_DOMAIN value : lb.rancher.cloud","title":"Manifest (for clusters with RBAC enabled)"},{"location":"tutorials/rdns/#testing-ingress-example","text":"$ cat ingress.yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: nginx spec: ingressClassName: nginx rules: - host: nginx.lb.rancher.cloud http: paths: - backend: serviceName: nginx servicePort: 80 $ kubectl apply -f ingress.yaml ingress.extensions \"nginx\" created Wait a moment until DNS has the ingress IP. The RDNS IP in this example is \u201c172.31.35.77\u201d. $ kubectl get ingress NAME HOSTS ADDRESS PORTS AGE nginx nginx.lb.rancher.cloud 172.31.42.211 80 2m $ kubectl run -it --rm --restart=Never --image=infoblox/dnstools:latest dnstools If you don't see a command prompt, try pressing enter. dnstools# dig @172.31.35.77 nginx.lb.rancher.cloud +short 172.31.42.211 dnstools#","title":"Testing ingress example"},{"location":"tutorials/rfc2136/","text":"Configuring RFC2136 provider \u00b6 This tutorial describes how to use the RFC2136 with either BIND or Windows DNS. Using with BIND \u00b6 To use external-dns with BIND: generate/procure a key, configure DNS and add a deployment of external-dns. Server credentials: \u00b6 RFC2136 was developed for and tested with BIND DNS server. This documentation assumes that you already have a configured and working server. If you don\u2019t, please check BIND documents or tutorials. If your DNS is provided for you, ask for a TSIG key authorized to update and transfer the zone you wish to update. The key will look something like below. Skip the next steps wrt BIND setup. key \"externaldns-key\" { algorithm hmac-sha256; secret \"96Ah/a2g0/nLeFGK+d/0tzQcccf9hCEIy34PoXX2Qg8=\"; }; If you are your own DNS administrator create a TSIG key. Use tsig-keygen -a hmac-sha256 externaldns or on older distributions dnssec-keygen -a HMAC-SHA256 -b 256 -n HOST externaldns . You will end up with a key printed to standard out like above (or in the case of dnssec-keygen in a file called Kexternaldns......key ). BIND Configuration: \u00b6 If you do not administer your own DNS, skip to RFC provider configuration Edit your named.conf file (or appropriate included file) and add/change the following. Make sure You are listening on the right interfaces. At least whatever interface external-dns will be communicating over and the interface that faces the internet. Add the key that you generated/was given to you above. Copy paste the four lines that you got (not the same as the example key) into your file. Create a zone for kubernetes. If you already have a zone, skip to the next step. (I put the zone in it\u2019s own subdirectory because named, which shouldn\u2019t be running as root, needs to create a journal file and the default zone directory isn\u2019t writeable by named). zone \"k8s.example.org\" { type master; file \"/etc/bind/pri/k8s/k8s.zone\"; }; Add your key to both transfer and update. For instance with our previous zone. zone \"k8s.example.org\" { type master; file \"/etc/bind/pri/k8s/k8s.zone\"; allow-transfer { key \"externaldns-key\"; }; update-policy { grant externaldns-key zonesub ANY; }; }; Create a zone file (k8s.zone): $TTL 60 ; 1 minute k8s.example.org IN SOA k8s.example.org. root.k8s.example.org. ( 16 ; serial 60 ; refresh (1 minute) 60 ; retry (1 minute) 60 ; expire (1 minute) 60 ; minimum (1 minute) ) NS ns.k8s.example.org. ns A 123.456.789.012 Reload (or restart) named Using external-dns \u00b6 To use external-dns add an ingress or a LoadBalancer service with a host that is part of the domain-filter. For example both of the following would produce A records. apiVersion: v1 kind: Service metadata: name: nginx annotations: external-dns.alpha.kubernetes.io/hostname: svc.example.org spec: type: LoadBalancer ports: - port: 80 targetPort: 80 selector: app: nginx --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: my-ingress spec: rules: - host: ingress.example.org http: paths: - path: / backend: serviceName: my-service servicePort: 8000 Custom TTL \u00b6 The default DNS record TTL (Time-To-Live) is 0 seconds. You can customize this value by setting the annotation external-dns.alpha.kubernetes.io/ttl . e.g., modify the service manifest YAML file above: apiVersion: v1 kind: Service metadata: name: nginx annotations: external-dns.alpha.kubernetes.io/hostname: nginx.external-dns-test.my-org.com external-dns.alpha.kubernetes.io/ttl: 60 spec: ... This will set the DNS record\u2019s TTL to 60 seconds. A default TTL for all records can be set using the the flag with a time in seconds, minutes or hours, such as --rfc2136-min-ttl=60s There are other annotation that can affect the generation of DNS records, but these are beyond the scope of this tutorial and are covered in the main documentation. Test with external-dns installed on local machine (optional) \u00b6 You may install external-dns and test on a local machine by running: external-dns --txt-owner-id k8s --provider rfc2136 --rfc2136-host=192.168.0.1 --rfc2136-port=53 --rfc2136-zone=k8s.example.org --rfc2136-tsig-secret=96Ah/a2g0/nLeFGK+d/0tzQcccf9hCEIy34PoXX2Qg8= --rfc2136-tsig-secret-alg=hmac-sha256 --rfc2136-tsig-keyname=externaldns-key --rfc2136-tsig-axfr --source ingress --once --domain-filter=k8s.example.org --dry-run - host should be the IP of your master DNS server. - tsig-secret should be changed to match your secret. - tsig-keyname needs to match the keyname you used (if you changed it). - domain-filter can be used as shown to filter the domains you wish to update. RFC2136 provider configuration: \u00b6 In order to use external-dns with your cluster you need to add a deployment with access to your ingress and service resources. The following are two example manifests with and without RBAC respectively. With RBAC: apiVersion: v1 kind: Namespace metadata: name: external-dns labels: name: external-dns --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: external-dns namespace: external-dns rules: - apiGroups: - \"\" resources: - services - endpoints - pods - nodes verbs: - get - watch - list - apiGroups: - extensions - networking.k8s.io resources: - ingresses verbs: - get - list - watch --- apiVersion: v1 kind: ServiceAccount metadata: name: external-dns namespace: external-dns --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: external-dns-viewer namespace: external-dns roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: external-dns subjects: - kind: ServiceAccount name: external-dns namespace: external-dns --- apiVersion: apps/v1 kind: Deployment metadata: name: external-dns namespace: external-dns spec: selector: matchLabels: app: external-dns template: metadata: labels: app: external-dns spec: serviceAccountName: external-dns containers: - name: external-dns image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --registry=txt - --txt-prefix=external-dns- - --txt-owner-id=k8s - --provider=rfc2136 - --rfc2136-host=192.168.0.1 - --rfc2136-port=53 - --rfc2136-zone=k8s.example.org - --rfc2136-tsig-secret=96Ah/a2g0/nLeFGK+d/0tzQcccf9hCEIy34PoXX2Qg8= - --rfc2136-tsig-secret-alg=hmac-sha256 - --rfc2136-tsig-keyname=externaldns-key - --rfc2136-tsig-axfr - --source=ingress - --domain-filter=k8s.example.org Without RBAC: apiVersion: v1 kind: Namespace metadata: name: external-dns labels: name: external-dns --- apiVersion: apps/v1 kind: Deployment metadata: name: external-dns namespace: external-dns spec: selector: matchLabels: app: external-dns template: metadata: labels: app: external-dns spec: containers: - name: external-dns image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --registry=txt - --txt-prefix=external-dns- - --txt-owner-id=k8s - --provider=rfc2136 - --rfc2136-host=192.168.0.1 - --rfc2136-port=53 - --rfc2136-zone=k8s.example.org - --rfc2136-tsig-secret=96Ah/a2g0/nLeFGK+d/0tzQcccf9hCEIy34PoXX2Qg8= - --rfc2136-tsig-secret-alg=hmac-sha256 - --rfc2136-tsig-keyname=externaldns-key - --rfc2136-tsig-axfr - --source=ingress - --domain-filter=k8s.example.org Microsoft DNS (Insecure Updates) \u00b6 While external-dns was not developed or tested against Microsoft DNS, it can be configured to work against it. YMMV. Insecure Updates \u00b6 DNS-side configuration \u00b6 Create a DNS zone Enable insecure dynamic updates for the zone Enable Zone Transfers to all servers external-dns configuration \u00b6 You\u2019ll want to configure external-dns similarly to the following: ... - --provider=rfc2136 - --rfc2136-host=192.168.0.1 - --rfc2136-port=53 - --rfc2136-zone=k8s.example.org - --rfc2136-insecure - --rfc2136-tsig-axfr # needed to enable zone transfers, which is required for deletion of records. ... Secure Updates Using RFC3645 (GSS-TSIG) \u00b6 DNS-side configuration \u00b6 Create a DNS zone Enable secure dynamic updates for the zone Enable Zone Transfers to all servers If you see any error messages which indicate that external-dns was somehow not able to fetch existing DNS records from your DNS server, this could mean that you forgot about step 3. Kerberos Configuration \u00b6 DNS with secure updates relies upon a valid Kerberos configuration running within the external-dns container. At this time, you will need to create a ConfigMap for the external-dns container to use and mount it in your deployment. Below is an example of a working Kerberos configuration inside a ConfigMap definition. This may be different depending on many factors in your environment: apiVersion : v1 kind : ConfigMap metadata : creationTimestamp : null name : krb5.conf data : krb5.conf : | [logging] default = FILE:/var/log/krb5libs.log kdc = FILE:/var/log/krb5kdc.log admin_server = FILE:/var/log/kadmind.log [libdefaults] dns_lookup_realm = false ticket_lifetime = 24h renew_lifetime = 7d forwardable = true rdns = false pkinit_anchors = /etc/pki/tls/certs/ca-bundle.crt default_ccache_name = KEYRING:persistent:%{uid} default_realm = YOUR-REALM.COM [realms] YOUR-REALM.COM = { kdc = dc1.yourdomain.com admin_server = dc1.yourdomain.com } [domain_realm] yourdomain.com = YOUR-REALM.COM .yourdomain.com = YOUR-REALM.COM In most cases, the realm name will probably be the same as the domain name, so you can simply replace YOUR-REALM.COM with something like YOURDOMAIN.COM . Once the ConfigMap is created, the container external-dns container needs to be told to mount that ConfigMap as a volume at the default Kerberos configuration location. The pod spec should include a similar configuration to the following: ... volumeMounts : - mountPath : /etc/krb5.conf name : kerberos-config-volume subPath : krb5.conf ... volumes : - configMap : defaultMode : 420 name : krb5.conf name : kerberos-config-volume ... external-dns configuration \u00b6 You\u2019ll want to configure external-dns similarly to the following: ... - --provider=rfc2136 - --rfc2136-gss-tsig - --rfc2136-host=dns-host.yourdomain.com - --rfc2136-port=53 - --rfc2136-zone=your-zone.com - --rfc2136-kerberos-username=your-domain-account - --rfc2136-kerberos-password=your-domain-password - --rfc2136-kerberos-realm=your-domain.com - --rfc2136-tsig-axfr # needed to enable zone transfers, which is required for deletion of records. ... As noted above, the --rfc2136-kerberos-realm flag is completely optional and won\u2019t be necessary in many cases. Most likely, you will only need it if you see errors similar to this: KRB Error: (68) KDC_ERR_WRONG_REALM Reserved for future use . The flag --rfc2136-host can be set to the host\u2019s domain name or IP address. However, it also determines the name of the Kerberos principal which is used during authentication. This means that Active Directory might only work if this is set to a specific domain name, possibly leading to errors like this: KDC_ERR_S_PRINCIPAL_UNKNOWN Server not found in Kerberos database . To fix this, try setting --rfc2136-host to the \u201cactual\u201d hostname of your DNS server.","title":"Configuring RFC2136 provider"},{"location":"tutorials/rfc2136/#configuring-rfc2136-provider","text":"This tutorial describes how to use the RFC2136 with either BIND or Windows DNS.","title":"Configuring RFC2136 provider"},{"location":"tutorials/rfc2136/#using-with-bind","text":"To use external-dns with BIND: generate/procure a key, configure DNS and add a deployment of external-dns.","title":"Using with BIND"},{"location":"tutorials/rfc2136/#server-credentials","text":"RFC2136 was developed for and tested with BIND DNS server. This documentation assumes that you already have a configured and working server. If you don\u2019t, please check BIND documents or tutorials. If your DNS is provided for you, ask for a TSIG key authorized to update and transfer the zone you wish to update. The key will look something like below. Skip the next steps wrt BIND setup. key \"externaldns-key\" { algorithm hmac-sha256; secret \"96Ah/a2g0/nLeFGK+d/0tzQcccf9hCEIy34PoXX2Qg8=\"; }; If you are your own DNS administrator create a TSIG key. Use tsig-keygen -a hmac-sha256 externaldns or on older distributions dnssec-keygen -a HMAC-SHA256 -b 256 -n HOST externaldns . You will end up with a key printed to standard out like above (or in the case of dnssec-keygen in a file called Kexternaldns......key ).","title":"Server credentials:"},{"location":"tutorials/rfc2136/#bind-configuration","text":"If you do not administer your own DNS, skip to RFC provider configuration Edit your named.conf file (or appropriate included file) and add/change the following. Make sure You are listening on the right interfaces. At least whatever interface external-dns will be communicating over and the interface that faces the internet. Add the key that you generated/was given to you above. Copy paste the four lines that you got (not the same as the example key) into your file. Create a zone for kubernetes. If you already have a zone, skip to the next step. (I put the zone in it\u2019s own subdirectory because named, which shouldn\u2019t be running as root, needs to create a journal file and the default zone directory isn\u2019t writeable by named). zone \"k8s.example.org\" { type master; file \"/etc/bind/pri/k8s/k8s.zone\"; }; Add your key to both transfer and update. For instance with our previous zone. zone \"k8s.example.org\" { type master; file \"/etc/bind/pri/k8s/k8s.zone\"; allow-transfer { key \"externaldns-key\"; }; update-policy { grant externaldns-key zonesub ANY; }; }; Create a zone file (k8s.zone): $TTL 60 ; 1 minute k8s.example.org IN SOA k8s.example.org. root.k8s.example.org. ( 16 ; serial 60 ; refresh (1 minute) 60 ; retry (1 minute) 60 ; expire (1 minute) 60 ; minimum (1 minute) ) NS ns.k8s.example.org. ns A 123.456.789.012 Reload (or restart) named","title":"BIND Configuration:"},{"location":"tutorials/rfc2136/#using-external-dns","text":"To use external-dns add an ingress or a LoadBalancer service with a host that is part of the domain-filter. For example both of the following would produce A records. apiVersion: v1 kind: Service metadata: name: nginx annotations: external-dns.alpha.kubernetes.io/hostname: svc.example.org spec: type: LoadBalancer ports: - port: 80 targetPort: 80 selector: app: nginx --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: my-ingress spec: rules: - host: ingress.example.org http: paths: - path: / backend: serviceName: my-service servicePort: 8000","title":"Using external-dns"},{"location":"tutorials/rfc2136/#custom-ttl","text":"The default DNS record TTL (Time-To-Live) is 0 seconds. You can customize this value by setting the annotation external-dns.alpha.kubernetes.io/ttl . e.g., modify the service manifest YAML file above: apiVersion: v1 kind: Service metadata: name: nginx annotations: external-dns.alpha.kubernetes.io/hostname: nginx.external-dns-test.my-org.com external-dns.alpha.kubernetes.io/ttl: 60 spec: ... This will set the DNS record\u2019s TTL to 60 seconds. A default TTL for all records can be set using the the flag with a time in seconds, minutes or hours, such as --rfc2136-min-ttl=60s There are other annotation that can affect the generation of DNS records, but these are beyond the scope of this tutorial and are covered in the main documentation.","title":"Custom TTL"},{"location":"tutorials/rfc2136/#test-with-external-dns-installed-on-local-machine-optional","text":"You may install external-dns and test on a local machine by running: external-dns --txt-owner-id k8s --provider rfc2136 --rfc2136-host=192.168.0.1 --rfc2136-port=53 --rfc2136-zone=k8s.example.org --rfc2136-tsig-secret=96Ah/a2g0/nLeFGK+d/0tzQcccf9hCEIy34PoXX2Qg8= --rfc2136-tsig-secret-alg=hmac-sha256 --rfc2136-tsig-keyname=externaldns-key --rfc2136-tsig-axfr --source ingress --once --domain-filter=k8s.example.org --dry-run - host should be the IP of your master DNS server. - tsig-secret should be changed to match your secret. - tsig-keyname needs to match the keyname you used (if you changed it). - domain-filter can be used as shown to filter the domains you wish to update.","title":"Test with external-dns installed on local machine (optional)"},{"location":"tutorials/rfc2136/#rfc2136-provider-configuration","text":"In order to use external-dns with your cluster you need to add a deployment with access to your ingress and service resources. The following are two example manifests with and without RBAC respectively. With RBAC: apiVersion: v1 kind: Namespace metadata: name: external-dns labels: name: external-dns --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: external-dns namespace: external-dns rules: - apiGroups: - \"\" resources: - services - endpoints - pods - nodes verbs: - get - watch - list - apiGroups: - extensions - networking.k8s.io resources: - ingresses verbs: - get - list - watch --- apiVersion: v1 kind: ServiceAccount metadata: name: external-dns namespace: external-dns --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: external-dns-viewer namespace: external-dns roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: external-dns subjects: - kind: ServiceAccount name: external-dns namespace: external-dns --- apiVersion: apps/v1 kind: Deployment metadata: name: external-dns namespace: external-dns spec: selector: matchLabels: app: external-dns template: metadata: labels: app: external-dns spec: serviceAccountName: external-dns containers: - name: external-dns image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --registry=txt - --txt-prefix=external-dns- - --txt-owner-id=k8s - --provider=rfc2136 - --rfc2136-host=192.168.0.1 - --rfc2136-port=53 - --rfc2136-zone=k8s.example.org - --rfc2136-tsig-secret=96Ah/a2g0/nLeFGK+d/0tzQcccf9hCEIy34PoXX2Qg8= - --rfc2136-tsig-secret-alg=hmac-sha256 - --rfc2136-tsig-keyname=externaldns-key - --rfc2136-tsig-axfr - --source=ingress - --domain-filter=k8s.example.org Without RBAC: apiVersion: v1 kind: Namespace metadata: name: external-dns labels: name: external-dns --- apiVersion: apps/v1 kind: Deployment metadata: name: external-dns namespace: external-dns spec: selector: matchLabels: app: external-dns template: metadata: labels: app: external-dns spec: containers: - name: external-dns image: registry.k8s.io/external-dns/external-dns:v0.13.5 args: - --registry=txt - --txt-prefix=external-dns- - --txt-owner-id=k8s - --provider=rfc2136 - --rfc2136-host=192.168.0.1 - --rfc2136-port=53 - --rfc2136-zone=k8s.example.org - --rfc2136-tsig-secret=96Ah/a2g0/nLeFGK+d/0tzQcccf9hCEIy34PoXX2Qg8= - --rfc2136-tsig-secret-alg=hmac-sha256 - --rfc2136-tsig-keyname=externaldns-key - --rfc2136-tsig-axfr - --source=ingress - --domain-filter=k8s.example.org","title":"RFC2136 provider configuration:"},{"location":"tutorials/rfc2136/#microsoft-dns-insecure-updates","text":"While external-dns was not developed or tested against Microsoft DNS, it can be configured to work against it. YMMV.","title":"Microsoft DNS (Insecure Updates)"},{"location":"tutorials/rfc2136/#insecure-updates","text":"","title":"Insecure Updates"},{"location":"tutorials/rfc2136/#dns-side-configuration","text":"Create a DNS zone Enable insecure dynamic updates for the zone Enable Zone Transfers to all servers","title":"DNS-side configuration"},{"location":"tutorials/rfc2136/#external-dns-configuration","text":"You\u2019ll want to configure external-dns similarly to the following: ... - --provider=rfc2136 - --rfc2136-host=192.168.0.1 - --rfc2136-port=53 - --rfc2136-zone=k8s.example.org - --rfc2136-insecure - --rfc2136-tsig-axfr # needed to enable zone transfers, which is required for deletion of records. ...","title":"external-dns configuration"},{"location":"tutorials/rfc2136/#secure-updates-using-rfc3645-gss-tsig","text":"","title":"Secure Updates Using RFC3645 (GSS-TSIG)"},{"location":"tutorials/rfc2136/#dns-side-configuration_1","text":"Create a DNS zone Enable secure dynamic updates for the zone Enable Zone Transfers to all servers If you see any error messages which indicate that external-dns was somehow not able to fetch existing DNS records from your DNS server, this could mean that you forgot about step 3.","title":"DNS-side configuration"},{"location":"tutorials/rfc2136/#kerberos-configuration","text":"DNS with secure updates relies upon a valid Kerberos configuration running within the external-dns container. At this time, you will need to create a ConfigMap for the external-dns container to use and mount it in your deployment. Below is an example of a working Kerberos configuration inside a ConfigMap definition. This may be different depending on many factors in your environment: apiVersion : v1 kind : ConfigMap metadata : creationTimestamp : null name : krb5.conf data : krb5.conf : | [logging] default = FILE:/var/log/krb5libs.log kdc = FILE:/var/log/krb5kdc.log admin_server = FILE:/var/log/kadmind.log [libdefaults] dns_lookup_realm = false ticket_lifetime = 24h renew_lifetime = 7d forwardable = true rdns = false pkinit_anchors = /etc/pki/tls/certs/ca-bundle.crt default_ccache_name = KEYRING:persistent:%{uid} default_realm = YOUR-REALM.COM [realms] YOUR-REALM.COM = { kdc = dc1.yourdomain.com admin_server = dc1.yourdomain.com } [domain_realm] yourdomain.com = YOUR-REALM.COM .yourdomain.com = YOUR-REALM.COM In most cases, the realm name will probably be the same as the domain name, so you can simply replace YOUR-REALM.COM with something like YOURDOMAIN.COM . Once the ConfigMap is created, the container external-dns container needs to be told to mount that ConfigMap as a volume at the default Kerberos configuration location. The pod spec should include a similar configuration to the following: ... volumeMounts : - mountPath : /etc/krb5.conf name : kerberos-config-volume subPath : krb5.conf ... volumes : - configMap : defaultMode : 420 name : krb5.conf name : kerberos-config-volume ...","title":"Kerberos Configuration"},{"location":"tutorials/rfc2136/#external-dns-configuration_1","text":"You\u2019ll want to configure external-dns similarly to the following: ... - --provider=rfc2136 - --rfc2136-gss-tsig - --rfc2136-host=dns-host.yourdomain.com - --rfc2136-port=53 - --rfc2136-zone=your-zone.com - --rfc2136-kerberos-username=your-domain-account - --rfc2136-kerberos-password=your-domain-password - --rfc2136-kerberos-realm=your-domain.com - --rfc2136-tsig-axfr # needed to enable zone transfers, which is required for deletion of records. ... As noted above, the --rfc2136-kerberos-realm flag is completely optional and won\u2019t be necessary in many cases. Most likely, you will only need it if you see errors similar to this: KRB Error: (68) KDC_ERR_WRONG_REALM Reserved for future use . The flag --rfc2136-host can be set to the host\u2019s domain name or IP address. However, it also determines the name of the Kerberos principal which is used during authentication. This means that Active Directory might only work if this is set to a specific domain name, possibly leading to errors like this: KDC_ERR_S_PRINCIPAL_UNKNOWN Server not found in Kerberos database . To fix this, try setting --rfc2136-host to the \u201cactual\u201d hostname of your DNS server.","title":"external-dns configuration"},{"location":"tutorials/scaleway/","text":"Setting up ExternalDNS for Services on Scaleway \u00b6 This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using Scaleway DNS. Make sure to use >=0.7.4 version of ExternalDNS for this tutorial. Warning : Scaleway DNS is currently in Public Beta and may not be suited for production usage. Importing a Domain into Scaleway DNS \u00b6 In order to use your domain, you need to import it into Scaleway DNS. If it\u2019s not already done, you can follow this documentation Once the domain is imported you can either use the root zone, or create a subzone to use. In this example we will use example.com as an example. Creating Scaleway Credentials \u00b6 To use ExternalDNS with Scaleway DNS, you need to create an API token (composed of the Access Key and the Secret Key). You can either use existing ones or you can create a new token, as explained in How to generate an API token or directly by going to the credentials page . Two environment variables are needed to run ExternalDNS with Scaleway DNS: - SCW_ACCESS_KEY which is the Access Key. - SCW_SECRET_KEY which is the Secret Key. Deploy ExternalDNS \u00b6 Connect your kubectl client to the cluster you want to test ExternalDNS with. Then apply one of the following manifests file to deploy ExternalDNS. The following example are suited for development. For a production usage, prefer secrets over environment, and use a tagged release . Manifest (for clusters without RBAC enabled) \u00b6 apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : replicas : 1 selector : matchLabels : app : external-dns strategy : type : Recreate template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=scaleway env : - name : SCW_ACCESS_KEY value : \"\" - name : SCW_SECRET_KEY value : \"\" Manifest (for clusters with RBAC enabled) \u00b6 apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" , \"watch\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : replicas : 1 selector : matchLabels : app : external-dns strategy : type : Recreate template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=scaleway env : - name : SCW_ACCESS_KEY value : \"\" - name : SCW_SECRET_KEY value : \"\" Deploying an Nginx Service \u00b6 Create a service file called \u2018nginx.yaml\u2019 with the following contents: apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : replicas : 1 selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 --- apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : my-app.example.com spec : selector : app : nginx type : LoadBalancer ports : - protocol : TCP port : 80 targetPort : 80 Note the annotation on the service; use the same hostname as the Scaleway DNS zone created above. ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records. Create the deployment and service: $ kubectl create -f nginx.yaml Depending where you run your service it can take a little while for your cloud provider to create an external IP for the service. Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize the Scaleway DNS records. Verifying Scaleway DNS records \u00b6 Check your Scaleway DNS UI to view the records for your Scaleway DNS zone. Click on the zone for the one created above if a different domain was used. This should show the external IP address of the service as the A record for your domain. Cleanup \u00b6 Now that we have verified that ExternalDNS will automatically manage Scaleway DNS records, we can delete the tutorial\u2019s example: $ kubectl delete service -f nginx.yaml $ kubectl delete service -f externaldns.yaml","title":"Setting up ExternalDNS for Services on Scaleway"},{"location":"tutorials/scaleway/#setting-up-externaldns-for-services-on-scaleway","text":"This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using Scaleway DNS. Make sure to use >=0.7.4 version of ExternalDNS for this tutorial. Warning : Scaleway DNS is currently in Public Beta and may not be suited for production usage.","title":"Setting up ExternalDNS for Services on Scaleway"},{"location":"tutorials/scaleway/#importing-a-domain-into-scaleway-dns","text":"In order to use your domain, you need to import it into Scaleway DNS. If it\u2019s not already done, you can follow this documentation Once the domain is imported you can either use the root zone, or create a subzone to use. In this example we will use example.com as an example.","title":"Importing a Domain into Scaleway DNS"},{"location":"tutorials/scaleway/#creating-scaleway-credentials","text":"To use ExternalDNS with Scaleway DNS, you need to create an API token (composed of the Access Key and the Secret Key). You can either use existing ones or you can create a new token, as explained in How to generate an API token or directly by going to the credentials page . Two environment variables are needed to run ExternalDNS with Scaleway DNS: - SCW_ACCESS_KEY which is the Access Key. - SCW_SECRET_KEY which is the Secret Key.","title":"Creating Scaleway Credentials"},{"location":"tutorials/scaleway/#deploy-externaldns","text":"Connect your kubectl client to the cluster you want to test ExternalDNS with. Then apply one of the following manifests file to deploy ExternalDNS. The following example are suited for development. For a production usage, prefer secrets over environment, and use a tagged release .","title":"Deploy ExternalDNS"},{"location":"tutorials/scaleway/#manifest-for-clusters-without-rbac-enabled","text":"apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : replicas : 1 selector : matchLabels : app : external-dns strategy : type : Recreate template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=scaleway env : - name : SCW_ACCESS_KEY value : \"\" - name : SCW_SECRET_KEY value : \"\"","title":"Manifest (for clusters without RBAC enabled)"},{"location":"tutorials/scaleway/#manifest-for-clusters-with-rbac-enabled","text":"apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" , \"watch\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : replicas : 1 selector : matchLabels : app : external-dns strategy : type : Recreate template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=scaleway env : - name : SCW_ACCESS_KEY value : \"\" - name : SCW_SECRET_KEY value : \"\"","title":"Manifest (for clusters with RBAC enabled)"},{"location":"tutorials/scaleway/#deploying-an-nginx-service","text":"Create a service file called \u2018nginx.yaml\u2019 with the following contents: apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : replicas : 1 selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 --- apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : my-app.example.com spec : selector : app : nginx type : LoadBalancer ports : - protocol : TCP port : 80 targetPort : 80 Note the annotation on the service; use the same hostname as the Scaleway DNS zone created above. ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records. Create the deployment and service: $ kubectl create -f nginx.yaml Depending where you run your service it can take a little while for your cloud provider to create an external IP for the service. Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize the Scaleway DNS records.","title":"Deploying an Nginx Service"},{"location":"tutorials/scaleway/#verifying-scaleway-dns-records","text":"Check your Scaleway DNS UI to view the records for your Scaleway DNS zone. Click on the zone for the one created above if a different domain was used. This should show the external IP address of the service as the A record for your domain.","title":"Verifying Scaleway DNS records"},{"location":"tutorials/scaleway/#cleanup","text":"Now that we have verified that ExternalDNS will automatically manage Scaleway DNS records, we can delete the tutorial\u2019s example: $ kubectl delete service -f nginx.yaml $ kubectl delete service -f externaldns.yaml","title":"Cleanup"},{"location":"tutorials/security-context/","text":"Running ExternalDNS with limited privileges \u00b6 You can run ExternalDNS with reduced privileges since v0.5.6 using the following SecurityContext . apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - ... # your arguments here securityContext : runAsNonRoot : true runAsUser : 65534 readOnlyRootFilesystem : true capabilities : drop : [ \"ALL\" ]","title":"Running ExternalDNS with limited privileges"},{"location":"tutorials/security-context/#running-externaldns-with-limited-privileges","text":"You can run ExternalDNS with reduced privileges since v0.5.6 using the following SecurityContext . apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - ... # your arguments here securityContext : runAsNonRoot : true runAsUser : 65534 readOnlyRootFilesystem : true capabilities : drop : [ \"ALL\" ]","title":"Running ExternalDNS with limited privileges"},{"location":"tutorials/tencentcloud/","text":"Setting up ExternalDNS for Tencent Cloud \u00b6 External Dns Version \u00b6 Make sure to use >=0.13.1 version of ExternalDNS for this tutorial Set up PrivateDns or DNSPod \u00b6 Tencent Cloud DNSPod Service is the domain name resolution and management service for public access. Tencent Cloud PrivateDNS Service is the domain name resolution and management service for VPC internal access. If you want to use internal dns service in Tencent Cloud. 1. Set up the args --tencent-cloud-zone-type=private 2. Create a DNS domain in PrivateDNS console. DNS domain which will contain the managed DNS records. If you want to use public dns service in Tencent Cloud. 1. Set up the args --tencent-cloud-zone-type=public 2. Create a Domain in DnsPod console. DNS domain which will contain the managed DNS records. Set up CAM for API Key \u00b6 In Tencent CAM Console. you may get the secretId and secretKey pair. make sure the key pair has those Policy. { \"version\" : \"2.0\" , \"statement\" : [ { \"effect\" : \"allow\" , \"action\" : [ \"dnspod:ModifyRecord\" , \"dnspod:DeleteRecord\" , \"dnspod:CreateRecord\" , \"dnspod:DescribeRecordList\" , \"dnspod:DescribeDomainList\" ], \"resource\" : [ \"*\" ] }, { \"effect\" : \"allow\" , \"action\" : [ \"privatedns:DescribePrivateZoneList\" , \"privatedns:DescribePrivateZoneRecordList\" , \"privatedns:CreatePrivateZoneRecord\" , \"privatedns:DeletePrivateZoneRecord\" , \"privatedns:ModifyPrivateZoneRecord\" ], \"resource\" : [ \"*\" ] } ] } Deploy ExternalDNS \u00b6 Manifest (for clusters with RBAC enabled) \u00b6 apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : v1 kind : ConfigMap metadata : name : external-dns data : tencent-cloud.json : | { \"regionId\": \"ap-shanghai\", \"secretId\": \"******\", \"secretKey\": \"******\", \"vpcId\": \"vpc-******\", \"internetEndpoint\": false # Default: false. Access the Tencent API through the intranet. If you need to deploy on the public network, you need to change to true } --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - args : - --source=service - --source=ingress - --domain-filter=external-dns-test.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones - --provider=tencentcloud - --policy=sync # set `upsert-only` would prevent ExternalDNS from deleting any records - --tencent-cloud-zone-type=private # only look at private hosted zones. set `public` to use the public dns service. - --tencent-cloud-config-file=/etc/kubernetes/tencent-cloud.json image : registry.k8s.io/external-dns/external-dns:v0.13.5 imagePullPolicy : Always name : external-dns resources : {} terminationMessagePath : /dev/termination-log terminationMessagePolicy : File volumeMounts : - mountPath : /etc/kubernetes name : config-volume readOnly : true dnsPolicy : ClusterFirst hostAliases : - hostnames : - privatedns.internal.tencentcloudapi.com - dnspod.internal.tencentcloudapi.com ip : 169.254.0.95 restartPolicy : Always schedulerName : default-scheduler securityContext : {} serviceAccount : external-dns serviceAccountName : external-dns terminationGracePeriodSeconds : 30 volumes : - configMap : defaultMode : 420 items : - key : tencent-cloud.json path : tencent-cloud.json name : external-dns name : config-volume Example \u00b6 Service \u00b6 apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : nginx.external-dns-test.com external-dns.alpha.kubernetes.io/internal-hostname : nginx-internal.external-dns-test.com external-dns.alpha.kubernetes.io/ttl : \"600\" spec : type : LoadBalancer ports : - port : 80 name : http targetPort : 80 selector : app : nginx --- apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 name : http nginx.external-dns-test.com will record to the Loadbalancer VIP. nginx-internal.external-dns-test.com will record to the ClusterIP. all of the DNS Record ttl will be 600. Attention \u00b6 This makes ExternalDNS safe for running in environments where there are other records managed via other means.","title":"Setting up ExternalDNS for Tencent Cloud"},{"location":"tutorials/tencentcloud/#setting-up-externaldns-for-tencent-cloud","text":"","title":"Setting up ExternalDNS for Tencent Cloud"},{"location":"tutorials/tencentcloud/#external-dns-version","text":"Make sure to use >=0.13.1 version of ExternalDNS for this tutorial","title":"External Dns Version"},{"location":"tutorials/tencentcloud/#set-up-privatedns-or-dnspod","text":"Tencent Cloud DNSPod Service is the domain name resolution and management service for public access. Tencent Cloud PrivateDNS Service is the domain name resolution and management service for VPC internal access. If you want to use internal dns service in Tencent Cloud. 1. Set up the args --tencent-cloud-zone-type=private 2. Create a DNS domain in PrivateDNS console. DNS domain which will contain the managed DNS records. If you want to use public dns service in Tencent Cloud. 1. Set up the args --tencent-cloud-zone-type=public 2. Create a Domain in DnsPod console. DNS domain which will contain the managed DNS records.","title":"Set up PrivateDns or DNSPod"},{"location":"tutorials/tencentcloud/#set-up-cam-for-api-key","text":"In Tencent CAM Console. you may get the secretId and secretKey pair. make sure the key pair has those Policy. { \"version\" : \"2.0\" , \"statement\" : [ { \"effect\" : \"allow\" , \"action\" : [ \"dnspod:ModifyRecord\" , \"dnspod:DeleteRecord\" , \"dnspod:CreateRecord\" , \"dnspod:DescribeRecordList\" , \"dnspod:DescribeDomainList\" ], \"resource\" : [ \"*\" ] }, { \"effect\" : \"allow\" , \"action\" : [ \"privatedns:DescribePrivateZoneList\" , \"privatedns:DescribePrivateZoneRecordList\" , \"privatedns:CreatePrivateZoneRecord\" , \"privatedns:DeletePrivateZoneRecord\" , \"privatedns:ModifyPrivateZoneRecord\" ], \"resource\" : [ \"*\" ] } ] }","title":"Set up CAM for API Key"},{"location":"tutorials/tencentcloud/#deploy-externaldns","text":"","title":"Deploy ExternalDNS"},{"location":"tutorials/tencentcloud/#manifest-for-clusters-with-rbac-enabled","text":"apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : v1 kind : ConfigMap metadata : name : external-dns data : tencent-cloud.json : | { \"regionId\": \"ap-shanghai\", \"secretId\": \"******\", \"secretKey\": \"******\", \"vpcId\": \"vpc-******\", \"internetEndpoint\": false # Default: false. Access the Tencent API through the intranet. If you need to deploy on the public network, you need to change to true } --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - args : - --source=service - --source=ingress - --domain-filter=external-dns-test.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones - --provider=tencentcloud - --policy=sync # set `upsert-only` would prevent ExternalDNS from deleting any records - --tencent-cloud-zone-type=private # only look at private hosted zones. set `public` to use the public dns service. - --tencent-cloud-config-file=/etc/kubernetes/tencent-cloud.json image : registry.k8s.io/external-dns/external-dns:v0.13.5 imagePullPolicy : Always name : external-dns resources : {} terminationMessagePath : /dev/termination-log terminationMessagePolicy : File volumeMounts : - mountPath : /etc/kubernetes name : config-volume readOnly : true dnsPolicy : ClusterFirst hostAliases : - hostnames : - privatedns.internal.tencentcloudapi.com - dnspod.internal.tencentcloudapi.com ip : 169.254.0.95 restartPolicy : Always schedulerName : default-scheduler securityContext : {} serviceAccount : external-dns serviceAccountName : external-dns terminationGracePeriodSeconds : 30 volumes : - configMap : defaultMode : 420 items : - key : tencent-cloud.json path : tencent-cloud.json name : external-dns name : config-volume","title":"Manifest (for clusters with RBAC enabled)"},{"location":"tutorials/tencentcloud/#example","text":"","title":"Example"},{"location":"tutorials/tencentcloud/#service","text":"apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : nginx.external-dns-test.com external-dns.alpha.kubernetes.io/internal-hostname : nginx-internal.external-dns-test.com external-dns.alpha.kubernetes.io/ttl : \"600\" spec : type : LoadBalancer ports : - port : 80 name : http targetPort : 80 selector : app : nginx --- apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 name : http nginx.external-dns-test.com will record to the Loadbalancer VIP. nginx-internal.external-dns-test.com will record to the ClusterIP. all of the DNS Record ttl will be 600.","title":"Service"},{"location":"tutorials/tencentcloud/#attention","text":"This makes ExternalDNS safe for running in environments where there are other records managed via other means.","title":"Attention"},{"location":"tutorials/traefik-proxy/","text":"Configuring ExternalDNS to use the Traefik Proxy Source \u00b6 This tutorial describes how to configure ExternalDNS to use the Traefik Proxy source. It is meant to supplement the other provider-specific setup tutorials. Manifest (for clusters without RBAC enabled) \u00b6 apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns # update this to the desired external-dns version image : registry.k8s.io/external-dns/external-dns:v0.13.3 args : - --source=traefik-proxy - --provider=aws - --registry=txt - --txt-owner-id=my-identifier Manifest (for clusters with RBAC enabled) \u00b6 apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" , \"watch\" ] - apiGroups : [ \"traefik.containo.us\" , \"traefik.io\" ] resources : [ \"ingressroutes\" , \"ingressroutetcps\" , \"ingressrouteudps\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns # update this to the desired external-dns version image : registry.k8s.io/external-dns/external-dns:v0.13.3 args : - --source=traefik-proxy - --provider=aws - --registry=txt - --txt-owner-id=my-identifier Deploying a Traefik IngressRoute \u00b6 Create a IngressRoute file called \u2018traefik-ingress.yaml\u2019 with the following contents: apiVersion : traefik.io/v1alpha1 kind : IngressRoute metadata : name : traefik-ingress annotations : external-dns.alpha.kubernetes.io/target : traefik.example.com kubernetes.io/ingress.class : traefik spec : entryPoints : - web - websecure routes : - match : Host(`application.example.com`) kind : Rule services : - name : service namespace : namespace port : port Note the annotation on the IngressRoute ( external-dns.alpha.kubernetes.io/target ); use the same hostname as the traefik DNS. ExternalDNS uses this annotation to determine what services should be registered with DNS. Create the IngressRoute: $ kubectl create -f traefik-ingress.yaml Depending where you run your IngressRoute it can take a little while for ExternalDNS synchronize the DNS record. Cleanup \u00b6 Now that we have verified that ExternalDNS will automatically manage Traefik DNS records, we can delete the tutorial\u2019s example: $ kubectl delete -f traefik-ingress.yaml $ kubectl delete -f externaldns.yaml","title":"Configuring ExternalDNS to use the Traefik Proxy Source"},{"location":"tutorials/traefik-proxy/#configuring-externaldns-to-use-the-traefik-proxy-source","text":"This tutorial describes how to configure ExternalDNS to use the Traefik Proxy source. It is meant to supplement the other provider-specific setup tutorials.","title":"Configuring ExternalDNS to use the Traefik Proxy Source"},{"location":"tutorials/traefik-proxy/#manifest-for-clusters-without-rbac-enabled","text":"apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns # update this to the desired external-dns version image : registry.k8s.io/external-dns/external-dns:v0.13.3 args : - --source=traefik-proxy - --provider=aws - --registry=txt - --txt-owner-id=my-identifier","title":"Manifest (for clusters without RBAC enabled)"},{"location":"tutorials/traefik-proxy/#manifest-for-clusters-with-rbac-enabled","text":"apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" , \"watch\" ] - apiGroups : [ \"traefik.containo.us\" , \"traefik.io\" ] resources : [ \"ingressroutes\" , \"ingressroutetcps\" , \"ingressrouteudps\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns # update this to the desired external-dns version image : registry.k8s.io/external-dns/external-dns:v0.13.3 args : - --source=traefik-proxy - --provider=aws - --registry=txt - --txt-owner-id=my-identifier","title":"Manifest (for clusters with RBAC enabled)"},{"location":"tutorials/traefik-proxy/#deploying-a-traefik-ingressroute","text":"Create a IngressRoute file called \u2018traefik-ingress.yaml\u2019 with the following contents: apiVersion : traefik.io/v1alpha1 kind : IngressRoute metadata : name : traefik-ingress annotations : external-dns.alpha.kubernetes.io/target : traefik.example.com kubernetes.io/ingress.class : traefik spec : entryPoints : - web - websecure routes : - match : Host(`application.example.com`) kind : Rule services : - name : service namespace : namespace port : port Note the annotation on the IngressRoute ( external-dns.alpha.kubernetes.io/target ); use the same hostname as the traefik DNS. ExternalDNS uses this annotation to determine what services should be registered with DNS. Create the IngressRoute: $ kubectl create -f traefik-ingress.yaml Depending where you run your IngressRoute it can take a little while for ExternalDNS synchronize the DNS record.","title":"Deploying a Traefik IngressRoute"},{"location":"tutorials/traefik-proxy/#cleanup","text":"Now that we have verified that ExternalDNS will automatically manage Traefik DNS records, we can delete the tutorial\u2019s example: $ kubectl delete -f traefik-ingress.yaml $ kubectl delete -f externaldns.yaml","title":"Cleanup"},{"location":"tutorials/transip/","text":"Setting up ExternalDNS for Services on TransIP \u00b6 This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using TransIP. Make sure to use >=0.5.14 version of ExternalDNS for this tutorial, have at least 1 domain registered at TransIP and enabled the API. Enable TransIP API and prepare your API key \u00b6 To use the TransIP API you need an account at TransIP and enable API usage as described in the knowledge base . With the private key generated by the API, we create a kubernetes secret: $ kubectl create secret generic transip-api-key --from-file = transip-api-key = /path/to/private.key Deploy ExternalDNS \u00b6 Below are example manifests, for both cluster without or with RBAC enabled. Don\u2019t forget to replace YOUR_TRANSIP_ACCOUNT_NAME with your TransIP account name. In these examples, an example domain-filter is defined. Such a filter can be used to prevent ExternalDNS from touching any domain not listed in the filter. Refer to the docs for any other command-line parameters you might want to use. Manifest (for clusters without RBAC enabled) \u00b6 apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains - --provider=transip - --transip-account=YOUR_TRANSIP_ACCOUNT_NAME - --transip-keyfile=/transip/transip-api-key volumeMounts : - mountPath : /transip name : transip-api-key readOnly : true volumes : - name : transip-api-key secret : secretName : transip-api-key Manifest (for clusters with RBAC enabled) \u00b6 apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"watch\" , \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains - --provider=transip - --transip-account=YOUR_TRANSIP_ACCOUNT_NAME - --transip-keyfile=/transip/transip-api-key volumeMounts : - mountPath : /transip name : transip-api-key readOnly : true volumes : - name : transip-api-key secret : secretName : transip-api-key Deploying an Nginx Service \u00b6 Create a service file called \u2018nginx.yaml\u2019 with the following contents: apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 --- apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : my-app.example.com spec : selector : app : nginx type : LoadBalancer ports : - protocol : TCP port : 80 targetPort : 80 Note the annotation on the service; this is the name ExternalDNS will create and manage DNS records for. ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records. Create the deployment and service: $ kubectl create -f nginx.yaml Depending where you run your service it can take a little while for your cloud provider to create an external IP for the service. Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize the TransIP DNS records. Verifying TransIP DNS records \u00b6 Check your TransIP Control Panel to view the records for your TransIP DNS zone. Click on the zone for the one created above if a different domain was used. This should show the external IP address of the service as the A record for your domain.","title":"Setting up ExternalDNS for Services on TransIP"},{"location":"tutorials/transip/#setting-up-externaldns-for-services-on-transip","text":"This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using TransIP. Make sure to use >=0.5.14 version of ExternalDNS for this tutorial, have at least 1 domain registered at TransIP and enabled the API.","title":"Setting up ExternalDNS for Services on TransIP"},{"location":"tutorials/transip/#enable-transip-api-and-prepare-your-api-key","text":"To use the TransIP API you need an account at TransIP and enable API usage as described in the knowledge base . With the private key generated by the API, we create a kubernetes secret: $ kubectl create secret generic transip-api-key --from-file = transip-api-key = /path/to/private.key","title":"Enable TransIP API and prepare your API key"},{"location":"tutorials/transip/#deploy-externaldns","text":"Below are example manifests, for both cluster without or with RBAC enabled. Don\u2019t forget to replace YOUR_TRANSIP_ACCOUNT_NAME with your TransIP account name. In these examples, an example domain-filter is defined. Such a filter can be used to prevent ExternalDNS from touching any domain not listed in the filter. Refer to the docs for any other command-line parameters you might want to use.","title":"Deploy ExternalDNS"},{"location":"tutorials/transip/#manifest-for-clusters-without-rbac-enabled","text":"apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains - --provider=transip - --transip-account=YOUR_TRANSIP_ACCOUNT_NAME - --transip-keyfile=/transip/transip-api-key volumeMounts : - mountPath : /transip name : transip-api-key readOnly : true volumes : - name : transip-api-key secret : secretName : transip-api-key","title":"Manifest (for clusters without RBAC enabled)"},{"location":"tutorials/transip/#manifest-for-clusters-with-rbac-enabled","text":"apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"watch\" , \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains - --provider=transip - --transip-account=YOUR_TRANSIP_ACCOUNT_NAME - --transip-keyfile=/transip/transip-api-key volumeMounts : - mountPath : /transip name : transip-api-key readOnly : true volumes : - name : transip-api-key secret : secretName : transip-api-key","title":"Manifest (for clusters with RBAC enabled)"},{"location":"tutorials/transip/#deploying-an-nginx-service","text":"Create a service file called \u2018nginx.yaml\u2019 with the following contents: apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 --- apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : my-app.example.com spec : selector : app : nginx type : LoadBalancer ports : - protocol : TCP port : 80 targetPort : 80 Note the annotation on the service; this is the name ExternalDNS will create and manage DNS records for. ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records. Create the deployment and service: $ kubectl create -f nginx.yaml Depending where you run your service it can take a little while for your cloud provider to create an external IP for the service. Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize the TransIP DNS records.","title":"Deploying an Nginx Service"},{"location":"tutorials/transip/#verifying-transip-dns-records","text":"Check your TransIP Control Panel to view the records for your TransIP DNS zone. Click on the zone for the one created above if a different domain was used. This should show the external IP address of the service as the A record for your domain.","title":"Verifying TransIP DNS records"},{"location":"tutorials/ultradns/","text":"Setting up ExternalDNS for Services on UltraDNS \u00b6 This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using UltraDNS. For this tutorial, please make sure that you are using a version > 0.7.2 of ExternalDNS. Managing DNS with UltraDNS \u00b6 If you would like to read-up on the UltraDNS service, you can find additional details here: Introduction to UltraDNS Before proceeding, please create a new DNS Zone that you will create your records in for this tutorial process. For the examples in this tutorial, we will be using example.com as our Zone. Setting Up UltraDNS Credentials \u00b6 The following environment variables will be needed to run ExternalDNS with UltraDNS. ULTRADNS_USERNAME , ULTRADNS_PASSWORD , & ULTRADNS_BASEURL ULTRADNS_ACCOUNTNAME (optional variable). Deploying ExternalDNS \u00b6 Connect your kubectl client to the cluster you want to test ExternalDNS with. Then, apply one of the following manifests file to deploy ExternalDNS. Note: We are assuming the zone is already present within UltraDNS. Note: While creating CNAMES as target endpoints, the --txt-prefix option is mandatory. Manifest (for clusters without RBAC enabled) \u00b6 apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service - --source=ingress # ingress is also possible - --domain-filter=example.com # (Recommended) We recommend to use this filter as it minimize the time to propagate changes, as there are less number of zones to look into.. - --provider=ultradns - --txt-prefix=txt- env : - name : ULTRADNS_USERNAME value : \"\" - name : ULTRADNS_PASSWORD # The password is required to be BASE64 encrypted. value : \"\" - name : ULTRADNS_BASEURL value : \"https://api.ultradns.com/\" - name : ULTRADNS_ACCOUNTNAME value : \"\" Manifest (for clusters with RBAC enabled) \u00b6 apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" , \"watch\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service - --source=ingress - --domain-filter=example.com #(Recommended) We recommend to use this filter as it minimize the time to propagate changes, as there are less number of zones to look into.. - --provider=ultradns - --txt-prefix=txt- env : - name : ULTRADNS_USERNAME value : \"\" - name : ULTRADNS_PASSWORD # The password is required to be BASE64 encrypted. value : \"\" - name : ULTRADNS_BASEURL value : \"https://api.ultradns.com/\" - name : ULTRADNS_ACCOUNTNAME value : \"\" Deploying an Nginx Service \u00b6 Create a service file called \u2018nginx.yaml\u2019 with the following contents: apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 --- apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : my-app.example.com. spec : selector : app : nginx type : LoadBalancer ports : - protocol : TCP port : 80 targetPort : 80 Please note the annotation on the service. Use the same hostname as the UltraDNS zone created above. ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records. Creating the Deployment and Service: \u00b6 $ kubectl create -f nginx.yaml $ kubectl create -f external-dns.yaml Depending on where you run your service from, it can take a few minutes for your cloud provider to create an external IP for the service. Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and will synchronize the UltraDNS records. Verifying UltraDNS Records \u00b6 Please verify on the UltraDNS UI that the records are created under the zone \u201cexample.com\u201d. For more information on UltraDNS UI, refer to (https://docs.ultradns.neustar/mspuserguide.html). Select the zone that was created above (or select the appropriate zone if a different zone was used.) The external IP address will be displayed as a CNAME record for your zone. Cleaning Up the Deployment and Service \u00b6 Now that we have verified that ExternalDNS will automatically manage your UltraDNS records, you can delete example zones that you created in this tutorial: $ kubectl delete service -f nginx.yaml $ kubectl delete service -f externaldns.yaml Examples to Manage your Records \u00b6 Creating Multiple A Records Target \u00b6 First, you want to create a service file called \u2018apple-banana-echo.yaml\u2019 --- kind : Pod apiVersion : v1 metadata : name : example-app labels : app : apple spec : containers : - name : example-app image : hashicorp/http-echo args : - \"-text=apple\" --- kind : Service apiVersion : v1 metadata : name : example-service spec : selector : app : apple ports : - port : 5678 # Default port for image Then, create service file called \u2018expose-apple-banana-app.yaml\u2019 to expose the services. For more information to deploy ingress controller, refer to (https://kubernetes.github.io/ingress-nginx/deploy/) apiVersion : networking.k8s.io/v1 kind : Ingress metadata : name : example-ingress annotations : ingress.kubernetes.io/rewrite-target : / ingress.kubernetes.io/scheme : internet-facing external-dns.alpha.kubernetes.io/hostname : apple.example.com. external-dns.alpha.kubernetes.io/target : 10.10.10.1,10.10.10.23 spec : rules : - http : paths : - path : /apple pathType : Prefix backend : service : name : example-service port : number : 5678 Then, create the deployment and service: $ kubectl create -f apple-banana-echo.yaml $ kubectl create -f expose-apple-banana-app.yaml $ kubectl create -f external-dns.yaml Depending on where you run your service from, it can take a few minutes for your cloud provider to create an external IP for the service. Please verify on the UltraDNS UI that the records have been created under the zone \u201cexample.com\u201d. Finally, you will need to clean up the deployment and service. Please verify on the UI afterwards that the records have been deleted from the zone \u201cexample.com\u201d: $ kubectl delete -f apple-banana-echo.yaml $ kubectl delete -f expose-apple-banana-app.yaml $ kubectl delete -f external-dns.yaml Creating CNAME Record \u00b6 Please note, that prior to deploying the external-dns service, you will need to add the option \u2013txt-prefix=txt- into external-dns.yaml. If this not provided, your records will not be created. First, create a service file called \u2018apple-banana-echo.yaml\u2019 Config File Example \u2013 kubernetes cluster is on-premise not on cloud --- kind : Pod apiVersion : v1 metadata : name : example-app labels : app : apple spec : containers : - name : example-app image : hashicorp/http-echo args : - \"-text=apple\" --- kind : Service apiVersion : v1 metadata : name : example-service spec : selector : app : apple ports : - port : 5678 # Default port for image --- apiVersion : networking.k8s.io/v1 kind : Ingress metadata : name : example-ingress annotations : ingress.kubernetes.io/rewrite-target : / ingress.kubernetes.io/scheme : internet-facing external-dns.alpha.kubernetes.io/hostname : apple.example.com. external-dns.alpha.kubernetes.io/target : apple.cname.com. spec : rules : - http : paths : - path : /apple backend : service : name : example-service port : number : 5678 Config File Example \u2013 Kubernetes cluster service from different cloud vendors --- kind : Pod apiVersion : v1 metadata : name : example-app labels : app : apple spec : containers : - name : example-app image : hashicorp/http-echo args : - \"-text=apple\" --- kind : Service apiVersion : v1 metadata : name : example-service annotations : external-dns.alpha.kubernetes.io/hostname : my-app.example.com. spec : selector : app : apple type : LoadBalancer ports : - protocol : TCP port : 5678 targetPort : 5678 Then, create the deployment and service: $ kubectl create -f apple-banana-echo.yaml $ kubectl create -f external-dns.yaml Depending on where you run your service from, it can take a few minutes for your cloud provider to create an external IP for the service. Please verify on the UltraDNS UI , that the records have been created under the zone \u201cexample.com\u201d. Finally, you will need to clean up the deployment and service. Please verify on the UI afterwards that the records have been deleted from the zone \u201cexample.com\u201d: $ kubectl delete -f apple-banana-echo.yaml $ kubectl delete -f external-dns.yaml Creating Multiple Types Of Records \u00b6 Please note, that prior to deploying the external-dns service, you will need to add the option \u2013txt-prefix=txt- into external-dns.yaml. Since you will also be created a CNAME record, If this not provided, your records will not be created. First, create a service file called \u2018apple-banana-echo.yaml\u2019 Config File Example \u2013 kubernetes cluster is on-premise not on cloud --- kind : Pod apiVersion : v1 metadata : name : example-app labels : app : apple spec : containers : - name : example-app image : hashicorp/http-echo args : - \"-text=apple\" --- kind : Service apiVersion : v1 metadata : name : example-service spec : selector : app : apple ports : - port : 5678 # Default port for image --- kind : Pod apiVersion : v1 metadata : name : example-app1 labels : app : apple1 spec : containers : - name : example-app1 image : hashicorp/http-echo args : - \"-text=apple\" --- kind : Service apiVersion : v1 metadata : name : example-service1 spec : selector : app : apple1 ports : - port : 5679 # Default port for image --- kind : Pod apiVersion : v1 metadata : name : example-app2 labels : app : apple2 spec : containers : - name : example-app2 image : hashicorp/http-echo args : - \"-text=apple\" --- kind : Service apiVersion : v1 metadata : name : example-service2 spec : selector : app : apple2 ports : - port : 5680 # Default port for image --- apiVersion : networking.k8s.io/v1 kind : Ingress metadata : name : example-ingress annotations : ingress.kubernetes.io/rewrite-target : / ingress.kubernetes.io/scheme : internet-facing external-dns.alpha.kubernetes.io/hostname : apple.example.com. external-dns.alpha.kubernetes.io/target : apple.cname.com. spec : rules : - http : paths : - path : /apple backend : service : name : example-service port : number : 5678 --- apiVersion : networking.k8s.io/v1 kind : Ingress metadata : name : example-ingress1 annotations : ingress.kubernetes.io/rewrite-target : / ingress.kubernetes.io/scheme : internet-facing external-dns.alpha.kubernetes.io/hostname : apple-banana.example.com. external-dns.alpha.kubernetes.io/target : 10.10.10.3 spec : rules : - http : paths : - path : /apple backend : service : name : example-service1 port : number : 5679 --- apiVersion : networking.k8s.io/v1 kind : Ingress metadata : name : example-ingress2 annotations : ingress.kubernetes.io/rewrite-target : / ingress.kubernetes.io/scheme : internet-facing external-dns.alpha.kubernetes.io/hostname : banana.example.com. external-dns.alpha.kubernetes.io/target : 10.10.10.3,10.10.10.20 spec : rules : - http : paths : - path : /apple backend : service : name : example-service2 port : number : 5680 Config File Example \u2013 Kubernetes cluster service from different cloud vendors --- apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 --- apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : my-app.example.com. spec : selector : app : nginx type : LoadBalancer ports : - protocol : TCP port : 80 targetPort : 80 --- kind : Pod apiVersion : v1 metadata : name : example-app labels : app : apple spec : containers : - name : example-app image : hashicorp/http-echo args : - \"-text=apple\" --- kind : Service apiVersion : v1 metadata : name : example-service spec : selector : app : apple ports : - port : 5678 # Default port for image --- kind : Pod apiVersion : v1 metadata : name : example-app1 labels : app : apple1 spec : containers : - name : example-app1 image : hashicorp/http-echo args : - \"-text=apple\" --- apiVersion : extensions/v1beta1 kind : Service apiVersion : v1 metadata : name : example-service1 spec : selector : app : apple1 ports : - port : 5679 # Default port for image --- apiVersion : networking.k8s.io/v1 kind : Ingress metadata : name : example-ingress annotations : ingress.kubernetes.io/rewrite-target : / ingress.kubernetes.io/scheme : internet-facing external-dns.alpha.kubernetes.io/hostname : apple.example.com. external-dns.alpha.kubernetes.io/target : 10.10.10.3,10.10.10.25 spec : rules : - http : paths : - path : /apple backend : service : name : example-service port : number : 5678 --- apiVersion : networking.k8s.io/v1 kind : Ingress metadata : name : example-ingress1 annotations : ingress.kubernetes.io/rewrite-target : / ingress.kubernetes.io/scheme : internet-facing external-dns.alpha.kubernetes.io/hostname : apple-banana.example.com. external-dns.alpha.kubernetes.io/target : 10.10.10.3 spec : rules : - http : paths : - path : /apple backend : service : name : example-service1 port : number : 5679 Then, create the deployment and service: $ kubectl create -f apple-banana-echo.yaml $ kubectl create -f external-dns.yaml Depending on where you run your service from, it can take a few minutes for your cloud provider to create an external IP for the service. Please verify on the UltraDNS UI , that the records have been created under the zone \u201cexample.com\u201d. Finally, you will need to clean up the deployment and service. Please verify on the UI afterwards that the records have been deleted from the zone \u201cexample.com\u201d: console $ kubectl delete -f apple-banana-echo.yaml $ kubectl delete -f external-dns.yaml","title":"Setting up ExternalDNS for Services on UltraDNS"},{"location":"tutorials/ultradns/#setting-up-externaldns-for-services-on-ultradns","text":"This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using UltraDNS. For this tutorial, please make sure that you are using a version > 0.7.2 of ExternalDNS.","title":"Setting up ExternalDNS for Services on UltraDNS"},{"location":"tutorials/ultradns/#managing-dns-with-ultradns","text":"If you would like to read-up on the UltraDNS service, you can find additional details here: Introduction to UltraDNS Before proceeding, please create a new DNS Zone that you will create your records in for this tutorial process. For the examples in this tutorial, we will be using example.com as our Zone.","title":"Managing DNS with UltraDNS"},{"location":"tutorials/ultradns/#setting-up-ultradns-credentials","text":"The following environment variables will be needed to run ExternalDNS with UltraDNS. ULTRADNS_USERNAME , ULTRADNS_PASSWORD , & ULTRADNS_BASEURL ULTRADNS_ACCOUNTNAME (optional variable).","title":"Setting Up UltraDNS Credentials"},{"location":"tutorials/ultradns/#deploying-externaldns","text":"Connect your kubectl client to the cluster you want to test ExternalDNS with. Then, apply one of the following manifests file to deploy ExternalDNS. Note: We are assuming the zone is already present within UltraDNS. Note: While creating CNAMES as target endpoints, the --txt-prefix option is mandatory.","title":"Deploying ExternalDNS"},{"location":"tutorials/ultradns/#manifest-for-clusters-without-rbac-enabled","text":"apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service - --source=ingress # ingress is also possible - --domain-filter=example.com # (Recommended) We recommend to use this filter as it minimize the time to propagate changes, as there are less number of zones to look into.. - --provider=ultradns - --txt-prefix=txt- env : - name : ULTRADNS_USERNAME value : \"\" - name : ULTRADNS_PASSWORD # The password is required to be BASE64 encrypted. value : \"\" - name : ULTRADNS_BASEURL value : \"https://api.ultradns.com/\" - name : ULTRADNS_ACCOUNTNAME value : \"\"","title":"Manifest (for clusters without RBAC enabled)"},{"location":"tutorials/ultradns/#manifest-for-clusters-with-rbac-enabled","text":"apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" , \"watch\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service - --source=ingress - --domain-filter=example.com #(Recommended) We recommend to use this filter as it minimize the time to propagate changes, as there are less number of zones to look into.. - --provider=ultradns - --txt-prefix=txt- env : - name : ULTRADNS_USERNAME value : \"\" - name : ULTRADNS_PASSWORD # The password is required to be BASE64 encrypted. value : \"\" - name : ULTRADNS_BASEURL value : \"https://api.ultradns.com/\" - name : ULTRADNS_ACCOUNTNAME value : \"\"","title":"Manifest (for clusters with RBAC enabled)"},{"location":"tutorials/ultradns/#deploying-an-nginx-service","text":"Create a service file called \u2018nginx.yaml\u2019 with the following contents: apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 --- apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : my-app.example.com. spec : selector : app : nginx type : LoadBalancer ports : - protocol : TCP port : 80 targetPort : 80 Please note the annotation on the service. Use the same hostname as the UltraDNS zone created above. ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records.","title":"Deploying an Nginx Service"},{"location":"tutorials/ultradns/#creating-the-deployment-and-service","text":"$ kubectl create -f nginx.yaml $ kubectl create -f external-dns.yaml Depending on where you run your service from, it can take a few minutes for your cloud provider to create an external IP for the service. Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and will synchronize the UltraDNS records.","title":"Creating the Deployment and Service:"},{"location":"tutorials/ultradns/#verifying-ultradns-records","text":"Please verify on the UltraDNS UI that the records are created under the zone \u201cexample.com\u201d. For more information on UltraDNS UI, refer to (https://docs.ultradns.neustar/mspuserguide.html). Select the zone that was created above (or select the appropriate zone if a different zone was used.) The external IP address will be displayed as a CNAME record for your zone.","title":"Verifying UltraDNS Records"},{"location":"tutorials/ultradns/#cleaning-up-the-deployment-and-service","text":"Now that we have verified that ExternalDNS will automatically manage your UltraDNS records, you can delete example zones that you created in this tutorial: $ kubectl delete service -f nginx.yaml $ kubectl delete service -f externaldns.yaml","title":"Cleaning Up the Deployment and Service"},{"location":"tutorials/ultradns/#examples-to-manage-your-records","text":"","title":"Examples to Manage your Records"},{"location":"tutorials/ultradns/#creating-multiple-a-records-target","text":"First, you want to create a service file called \u2018apple-banana-echo.yaml\u2019 --- kind : Pod apiVersion : v1 metadata : name : example-app labels : app : apple spec : containers : - name : example-app image : hashicorp/http-echo args : - \"-text=apple\" --- kind : Service apiVersion : v1 metadata : name : example-service spec : selector : app : apple ports : - port : 5678 # Default port for image Then, create service file called \u2018expose-apple-banana-app.yaml\u2019 to expose the services. For more information to deploy ingress controller, refer to (https://kubernetes.github.io/ingress-nginx/deploy/) apiVersion : networking.k8s.io/v1 kind : Ingress metadata : name : example-ingress annotations : ingress.kubernetes.io/rewrite-target : / ingress.kubernetes.io/scheme : internet-facing external-dns.alpha.kubernetes.io/hostname : apple.example.com. external-dns.alpha.kubernetes.io/target : 10.10.10.1,10.10.10.23 spec : rules : - http : paths : - path : /apple pathType : Prefix backend : service : name : example-service port : number : 5678 Then, create the deployment and service: $ kubectl create -f apple-banana-echo.yaml $ kubectl create -f expose-apple-banana-app.yaml $ kubectl create -f external-dns.yaml Depending on where you run your service from, it can take a few minutes for your cloud provider to create an external IP for the service. Please verify on the UltraDNS UI that the records have been created under the zone \u201cexample.com\u201d. Finally, you will need to clean up the deployment and service. Please verify on the UI afterwards that the records have been deleted from the zone \u201cexample.com\u201d: $ kubectl delete -f apple-banana-echo.yaml $ kubectl delete -f expose-apple-banana-app.yaml $ kubectl delete -f external-dns.yaml","title":"Creating Multiple A Records Target"},{"location":"tutorials/ultradns/#creating-cname-record","text":"Please note, that prior to deploying the external-dns service, you will need to add the option \u2013txt-prefix=txt- into external-dns.yaml. If this not provided, your records will not be created. First, create a service file called \u2018apple-banana-echo.yaml\u2019 Config File Example \u2013 kubernetes cluster is on-premise not on cloud --- kind : Pod apiVersion : v1 metadata : name : example-app labels : app : apple spec : containers : - name : example-app image : hashicorp/http-echo args : - \"-text=apple\" --- kind : Service apiVersion : v1 metadata : name : example-service spec : selector : app : apple ports : - port : 5678 # Default port for image --- apiVersion : networking.k8s.io/v1 kind : Ingress metadata : name : example-ingress annotations : ingress.kubernetes.io/rewrite-target : / ingress.kubernetes.io/scheme : internet-facing external-dns.alpha.kubernetes.io/hostname : apple.example.com. external-dns.alpha.kubernetes.io/target : apple.cname.com. spec : rules : - http : paths : - path : /apple backend : service : name : example-service port : number : 5678 Config File Example \u2013 Kubernetes cluster service from different cloud vendors --- kind : Pod apiVersion : v1 metadata : name : example-app labels : app : apple spec : containers : - name : example-app image : hashicorp/http-echo args : - \"-text=apple\" --- kind : Service apiVersion : v1 metadata : name : example-service annotations : external-dns.alpha.kubernetes.io/hostname : my-app.example.com. spec : selector : app : apple type : LoadBalancer ports : - protocol : TCP port : 5678 targetPort : 5678 Then, create the deployment and service: $ kubectl create -f apple-banana-echo.yaml $ kubectl create -f external-dns.yaml Depending on where you run your service from, it can take a few minutes for your cloud provider to create an external IP for the service. Please verify on the UltraDNS UI , that the records have been created under the zone \u201cexample.com\u201d. Finally, you will need to clean up the deployment and service. Please verify on the UI afterwards that the records have been deleted from the zone \u201cexample.com\u201d: $ kubectl delete -f apple-banana-echo.yaml $ kubectl delete -f external-dns.yaml","title":"Creating CNAME Record"},{"location":"tutorials/ultradns/#creating-multiple-types-of-records","text":"Please note, that prior to deploying the external-dns service, you will need to add the option \u2013txt-prefix=txt- into external-dns.yaml. Since you will also be created a CNAME record, If this not provided, your records will not be created. First, create a service file called \u2018apple-banana-echo.yaml\u2019 Config File Example \u2013 kubernetes cluster is on-premise not on cloud --- kind : Pod apiVersion : v1 metadata : name : example-app labels : app : apple spec : containers : - name : example-app image : hashicorp/http-echo args : - \"-text=apple\" --- kind : Service apiVersion : v1 metadata : name : example-service spec : selector : app : apple ports : - port : 5678 # Default port for image --- kind : Pod apiVersion : v1 metadata : name : example-app1 labels : app : apple1 spec : containers : - name : example-app1 image : hashicorp/http-echo args : - \"-text=apple\" --- kind : Service apiVersion : v1 metadata : name : example-service1 spec : selector : app : apple1 ports : - port : 5679 # Default port for image --- kind : Pod apiVersion : v1 metadata : name : example-app2 labels : app : apple2 spec : containers : - name : example-app2 image : hashicorp/http-echo args : - \"-text=apple\" --- kind : Service apiVersion : v1 metadata : name : example-service2 spec : selector : app : apple2 ports : - port : 5680 # Default port for image --- apiVersion : networking.k8s.io/v1 kind : Ingress metadata : name : example-ingress annotations : ingress.kubernetes.io/rewrite-target : / ingress.kubernetes.io/scheme : internet-facing external-dns.alpha.kubernetes.io/hostname : apple.example.com. external-dns.alpha.kubernetes.io/target : apple.cname.com. spec : rules : - http : paths : - path : /apple backend : service : name : example-service port : number : 5678 --- apiVersion : networking.k8s.io/v1 kind : Ingress metadata : name : example-ingress1 annotations : ingress.kubernetes.io/rewrite-target : / ingress.kubernetes.io/scheme : internet-facing external-dns.alpha.kubernetes.io/hostname : apple-banana.example.com. external-dns.alpha.kubernetes.io/target : 10.10.10.3 spec : rules : - http : paths : - path : /apple backend : service : name : example-service1 port : number : 5679 --- apiVersion : networking.k8s.io/v1 kind : Ingress metadata : name : example-ingress2 annotations : ingress.kubernetes.io/rewrite-target : / ingress.kubernetes.io/scheme : internet-facing external-dns.alpha.kubernetes.io/hostname : banana.example.com. external-dns.alpha.kubernetes.io/target : 10.10.10.3,10.10.10.20 spec : rules : - http : paths : - path : /apple backend : service : name : example-service2 port : number : 5680 Config File Example \u2013 Kubernetes cluster service from different cloud vendors --- apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 --- apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : my-app.example.com. spec : selector : app : nginx type : LoadBalancer ports : - protocol : TCP port : 80 targetPort : 80 --- kind : Pod apiVersion : v1 metadata : name : example-app labels : app : apple spec : containers : - name : example-app image : hashicorp/http-echo args : - \"-text=apple\" --- kind : Service apiVersion : v1 metadata : name : example-service spec : selector : app : apple ports : - port : 5678 # Default port for image --- kind : Pod apiVersion : v1 metadata : name : example-app1 labels : app : apple1 spec : containers : - name : example-app1 image : hashicorp/http-echo args : - \"-text=apple\" --- apiVersion : extensions/v1beta1 kind : Service apiVersion : v1 metadata : name : example-service1 spec : selector : app : apple1 ports : - port : 5679 # Default port for image --- apiVersion : networking.k8s.io/v1 kind : Ingress metadata : name : example-ingress annotations : ingress.kubernetes.io/rewrite-target : / ingress.kubernetes.io/scheme : internet-facing external-dns.alpha.kubernetes.io/hostname : apple.example.com. external-dns.alpha.kubernetes.io/target : 10.10.10.3,10.10.10.25 spec : rules : - http : paths : - path : /apple backend : service : name : example-service port : number : 5678 --- apiVersion : networking.k8s.io/v1 kind : Ingress metadata : name : example-ingress1 annotations : ingress.kubernetes.io/rewrite-target : / ingress.kubernetes.io/scheme : internet-facing external-dns.alpha.kubernetes.io/hostname : apple-banana.example.com. external-dns.alpha.kubernetes.io/target : 10.10.10.3 spec : rules : - http : paths : - path : /apple backend : service : name : example-service1 port : number : 5679 Then, create the deployment and service: $ kubectl create -f apple-banana-echo.yaml $ kubectl create -f external-dns.yaml Depending on where you run your service from, it can take a few minutes for your cloud provider to create an external IP for the service. Please verify on the UltraDNS UI , that the records have been created under the zone \u201cexample.com\u201d. Finally, you will need to clean up the deployment and service. Please verify on the UI afterwards that the records have been deleted from the zone \u201cexample.com\u201d: console $ kubectl delete -f apple-banana-echo.yaml $ kubectl delete -f external-dns.yaml","title":"Creating Multiple Types Of Records"},{"location":"tutorials/vinyldns/","text":"Setting up ExternalDNS for VinylDNS \u00b6 This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using VinylDNS. The environment vars VINYLDNS_ACCESS_KEY , VINYLDNS_SECRET_KEY , and VINYLDNS_HOST will be needed to run ExternalDNS with VinylDNS. Create a sample deployment and service for external-dns to use \u00b6 Run an application and expose it via a Kubernetes Service: $ kubectl run nginx --image = nginx --replicas = 1 --port = 80 $ kubectl expose deployment nginx --port = 80 --target-port = 80 --type = LoadBalancer Annotate the Service with your desired external DNS name. Make sure to change example.org to your domain. $ kubectl annotate service nginx \"external-dns.alpha.kubernetes.io/hostname=nginx.example.org.\" After the service is up and running, it should get an EXTERNAL-IP. At first this may showing as $ kubectl get svc NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes 10.0.0.1 443/TCP 1h nginx 10.0.0.115 80:30543/TCP 10s Once it\u2019s available % kubectl get svc NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes 10.0.0.1 443/TCP 1h nginx 10.0.0.115 34.x.x.x 80:30543/TCP 2m Deploy ExternalDNS to Kubernetes \u00b6 Connect your kubectl client to the cluster you want to test ExternalDNS with. Then apply one of the following manifests file to deploy ExternalDNS. Note for examples below When using registry=txt option, make sure to also use the txt-prefix and txt-owner-id options as well. If you try to create a TXT record in VinylDNS without a prefix, it will try to create a TXT record with the same name as your actual DNS record and fail (creating a stranded record external-dns cannot manage). Manifest (for clusters without RBAC enabled) \u00b6 apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --provider=vinyldns - --source=service - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --registry=txt - --txt-owner-id=grizz - --txt-prefix=txt- env : - name : VINYLDNS_HOST value : \"YOUR_VINYLDNS_HOST\" - name : VINYLDNS_ACCESS_KEY value : \"YOUR_VINYLDNS_ACCESS_KEY\" - name : VINYLDNS_SECRET_KEY value : \"YOUR_VINYLDNS_SECRET_KEY\" Manifest (for clusters with RBAC enabled) \u00b6 apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --provider=vinyldns - --source=service - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --registry=txt - --txt-owner-id=grizz - --txt-prefix=txt- env : env : - name : VINYLDNS_HOST value : \"YOUR_VINYLDNS_HOST\" - name : VINYLDNS_ACCESS_KEY value : \"YOUR_VINYLDNS_ACCESS_KEY\" - name : VINYLDNS_SECRET_KEY value : \"YOUR_VINYLDNS_SECRET_KEYY Running a locally built version pointed to the above nginx service \u00b6 Make sure your kubectl is configured correctly. Assuming you have the sources, build and run it like below. The vinyl access details needs to exported to the environment before running. make # output skipped export VINYLDNS_HOST = export VINYLDNS_ACCESS_KEY = export VINYLDNS_SECRET_KEY = ./build/external-dns \\ --provider = vinyldns \\ --source = service \\ --domain-filter = elements.capsps.comcast.net. \\ --zone-id-filter = 20e8bfd2-3a70-4e1b-8e11-c9c1948528d3 \\ --registry = txt \\ --txt-owner-id = grizz \\ --txt-prefix = txt- \\ --namespace = default \\ --once \\ --dry-run \\ --log-level debug INFO [ 0000 ] running in dry-run mode. No changes to DNS records will be made. INFO [ 0000 ] Created Kubernetes client https://some-k8s-cluster.example.com INFO [ 0001 ] Zone: [ nginx.example.org. ] # output skipped Having --dry-run=true and --log-level=debug is a great way to see exactly what VinylDNS is doing or is about to do.","title":"Setting up ExternalDNS for VinylDNS"},{"location":"tutorials/vinyldns/#setting-up-externaldns-for-vinyldns","text":"This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using VinylDNS. The environment vars VINYLDNS_ACCESS_KEY , VINYLDNS_SECRET_KEY , and VINYLDNS_HOST will be needed to run ExternalDNS with VinylDNS.","title":"Setting up ExternalDNS for VinylDNS"},{"location":"tutorials/vinyldns/#create-a-sample-deployment-and-service-for-external-dns-to-use","text":"Run an application and expose it via a Kubernetes Service: $ kubectl run nginx --image = nginx --replicas = 1 --port = 80 $ kubectl expose deployment nginx --port = 80 --target-port = 80 --type = LoadBalancer Annotate the Service with your desired external DNS name. Make sure to change example.org to your domain. $ kubectl annotate service nginx \"external-dns.alpha.kubernetes.io/hostname=nginx.example.org.\" After the service is up and running, it should get an EXTERNAL-IP. At first this may showing as $ kubectl get svc NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes 10.0.0.1 443/TCP 1h nginx 10.0.0.115 80:30543/TCP 10s Once it\u2019s available % kubectl get svc NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes 10.0.0.1 443/TCP 1h nginx 10.0.0.115 34.x.x.x 80:30543/TCP 2m","title":"Create a sample deployment and service for external-dns to use"},{"location":"tutorials/vinyldns/#deploy-externaldns-to-kubernetes","text":"Connect your kubectl client to the cluster you want to test ExternalDNS with. Then apply one of the following manifests file to deploy ExternalDNS. Note for examples below When using registry=txt option, make sure to also use the txt-prefix and txt-owner-id options as well. If you try to create a TXT record in VinylDNS without a prefix, it will try to create a TXT record with the same name as your actual DNS record and fail (creating a stranded record external-dns cannot manage).","title":"Deploy ExternalDNS to Kubernetes"},{"location":"tutorials/vinyldns/#manifest-for-clusters-without-rbac-enabled","text":"apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --provider=vinyldns - --source=service - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --registry=txt - --txt-owner-id=grizz - --txt-prefix=txt- env : - name : VINYLDNS_HOST value : \"YOUR_VINYLDNS_HOST\" - name : VINYLDNS_ACCESS_KEY value : \"YOUR_VINYLDNS_ACCESS_KEY\" - name : VINYLDNS_SECRET_KEY value : \"YOUR_VINYLDNS_SECRET_KEY\"","title":"Manifest (for clusters without RBAC enabled)"},{"location":"tutorials/vinyldns/#manifest-for-clusters-with-rbac-enabled","text":"apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --provider=vinyldns - --source=service - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --registry=txt - --txt-owner-id=grizz - --txt-prefix=txt- env : env : - name : VINYLDNS_HOST value : \"YOUR_VINYLDNS_HOST\" - name : VINYLDNS_ACCESS_KEY value : \"YOUR_VINYLDNS_ACCESS_KEY\" - name : VINYLDNS_SECRET_KEY value : \"YOUR_VINYLDNS_SECRET_KEYY","title":"Manifest (for clusters with RBAC enabled)"},{"location":"tutorials/vinyldns/#running-a-locally-built-version-pointed-to-the-above-nginx-service","text":"Make sure your kubectl is configured correctly. Assuming you have the sources, build and run it like below. The vinyl access details needs to exported to the environment before running. make # output skipped export VINYLDNS_HOST = export VINYLDNS_ACCESS_KEY = export VINYLDNS_SECRET_KEY = ./build/external-dns \\ --provider = vinyldns \\ --source = service \\ --domain-filter = elements.capsps.comcast.net. \\ --zone-id-filter = 20e8bfd2-3a70-4e1b-8e11-c9c1948528d3 \\ --registry = txt \\ --txt-owner-id = grizz \\ --txt-prefix = txt- \\ --namespace = default \\ --once \\ --dry-run \\ --log-level debug INFO [ 0000 ] running in dry-run mode. No changes to DNS records will be made. INFO [ 0000 ] Created Kubernetes client https://some-k8s-cluster.example.com INFO [ 0001 ] Zone: [ nginx.example.org. ] # output skipped Having --dry-run=true and --log-level=debug is a great way to see exactly what VinylDNS is doing or is about to do.","title":"Running a locally built version pointed to the above nginx service"},{"location":"tutorials/vultr/","text":"Setting up ExternalDNS for Services on Vultr \u00b6 This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using Vultr DNS. Make sure to use >=0.6 version of ExternalDNS for this tutorial. Managing DNS with Vultr \u00b6 If you want to read up on vultr DNS service you can read the following tutorial: Introduction to Vultr DNS Create a new DNS Zone where you want to create your records in. For the examples we will be using example.com Creating Vultr Credentials \u00b6 You will need to create a new API Key which can be found on the Vultr Dashboard . The environment variable VULTR_API_KEY will be needed to run ExternalDNS with Vultr. Deploy ExternalDNS \u00b6 Connect your kubectl client to the cluster you want to test ExternalDNS with. Then apply one of the following manifests file to deploy ExternalDNS. Manifest (for clusters without RBAC enabled) \u00b6 apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=vultr env : - name : VULTR_API_KEY value : \"YOU_VULTR_API_KEY\" Manifest (for clusters with RBAC enabled) \u00b6 apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=vultr env : - name : VULTR_API_KEY value : \"YOU_VULTR_API_KEY\" Deploying a Nginx Service \u00b6 Create a service file called \u2018nginx.yaml\u2019 with the following contents: apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 --- apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : my-app.example.com spec : selector : app : nginx type : LoadBalancer ports : - protocol : TCP port : 80 targetPort : 80 Note the annotation on the service; use the same hostname as the Vultr DNS zone created above. ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records. Create the deployment and service: $ kubectl create -f nginx.yaml Depending where you run your service it can take a little while for your cloud provider to create an external IP for the service. Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize the Vultr DNS records. Verifying Vultr DNS records \u00b6 Check your Vultr UI to view the records for your Vultr DNS zone. Click on the zone for the one created above if a different domain was used. This should show the external IP address of the service as the A record for your domain. Cleanup \u00b6 Now that we have verified that ExternalDNS will automatically manage Vultr DNS records, we can delete the tutorial\u2019s example: $ kubectl delete service -f nginx.yaml $ kubectl delete service -f externaldns.yaml","title":"Setting up ExternalDNS for Services on Vultr"},{"location":"tutorials/vultr/#setting-up-externaldns-for-services-on-vultr","text":"This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using Vultr DNS. Make sure to use >=0.6 version of ExternalDNS for this tutorial.","title":"Setting up ExternalDNS for Services on Vultr"},{"location":"tutorials/vultr/#managing-dns-with-vultr","text":"If you want to read up on vultr DNS service you can read the following tutorial: Introduction to Vultr DNS Create a new DNS Zone where you want to create your records in. For the examples we will be using example.com","title":"Managing DNS with Vultr"},{"location":"tutorials/vultr/#creating-vultr-credentials","text":"You will need to create a new API Key which can be found on the Vultr Dashboard . The environment variable VULTR_API_KEY will be needed to run ExternalDNS with Vultr.","title":"Creating Vultr Credentials"},{"location":"tutorials/vultr/#deploy-externaldns","text":"Connect your kubectl client to the cluster you want to test ExternalDNS with. Then apply one of the following manifests file to deploy ExternalDNS.","title":"Deploy ExternalDNS"},{"location":"tutorials/vultr/#manifest-for-clusters-without-rbac-enabled","text":"apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=vultr env : - name : VULTR_API_KEY value : \"YOU_VULTR_API_KEY\"","title":"Manifest (for clusters without RBAC enabled)"},{"location":"tutorials/vultr/#manifest-for-clusters-with-rbac-enabled","text":"apiVersion : v1 kind : ServiceAccount metadata : name : external-dns --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRole metadata : name : external-dns rules : - apiGroups : [ \"\" ] resources : [ \"services\" , \"endpoints\" , \"pods\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"extensions\" , \"networking.k8s.io\" ] resources : [ \"ingresses\" ] verbs : [ \"get\" , \"watch\" , \"list\" ] - apiGroups : [ \"\" ] resources : [ \"nodes\" ] verbs : [ \"list\" ] --- apiVersion : rbac.authorization.k8s.io/v1 kind : ClusterRoleBinding metadata : name : external-dns-viewer roleRef : apiGroup : rbac.authorization.k8s.io kind : ClusterRole name : external-dns subjects : - kind : ServiceAccount name : external-dns namespace : default --- apiVersion : apps/v1 kind : Deployment metadata : name : external-dns spec : strategy : type : Recreate selector : matchLabels : app : external-dns template : metadata : labels : app : external-dns spec : serviceAccountName : external-dns containers : - name : external-dns image : registry.k8s.io/external-dns/external-dns:v0.13.5 args : - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=vultr env : - name : VULTR_API_KEY value : \"YOU_VULTR_API_KEY\"","title":"Manifest (for clusters with RBAC enabled)"},{"location":"tutorials/vultr/#deploying-a-nginx-service","text":"Create a service file called \u2018nginx.yaml\u2019 with the following contents: apiVersion : apps/v1 kind : Deployment metadata : name : nginx spec : selector : matchLabels : app : nginx template : metadata : labels : app : nginx spec : containers : - image : nginx name : nginx ports : - containerPort : 80 --- apiVersion : v1 kind : Service metadata : name : nginx annotations : external-dns.alpha.kubernetes.io/hostname : my-app.example.com spec : selector : app : nginx type : LoadBalancer ports : - protocol : TCP port : 80 targetPort : 80 Note the annotation on the service; use the same hostname as the Vultr DNS zone created above. ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records. Create the deployment and service: $ kubectl create -f nginx.yaml Depending where you run your service it can take a little while for your cloud provider to create an external IP for the service. Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize the Vultr DNS records.","title":"Deploying a Nginx Service"},{"location":"tutorials/vultr/#verifying-vultr-dns-records","text":"Check your Vultr UI to view the records for your Vultr DNS zone. Click on the zone for the one created above if a different domain was used. This should show the external IP address of the service as the A record for your domain.","title":"Verifying Vultr DNS records"},{"location":"tutorials/vultr/#cleanup","text":"Now that we have verified that ExternalDNS will automatically manage Vultr DNS records, we can delete the tutorial\u2019s example: $ kubectl delete service -f nginx.yaml $ kubectl delete service -f externaldns.yaml","title":"Cleanup"}]} \ No newline at end of file diff --git a/v0.13.6/sitemap.xml b/v0.13.6/sitemap.xml new file mode 100644 index 0000000000..c273e7da6b --- /dev/null +++ b/v0.13.6/sitemap.xml @@ -0,0 +1,383 @@ + + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + + None + 2023-09-06 + daily + + \ No newline at end of file diff --git a/v0.13.6/sitemap.xml.gz b/v0.13.6/sitemap.xml.gz new file mode 100644 index 0000000000000000000000000000000000000000..caa47f44270682d3707e5cd5ee68c79e8251b081 GIT binary patch literal 244 zcmb2|=HSp({*l7;zc{lbH8-(9uOc^x;q6)Pyk-ND)`yj0jW+DoD?>~t7N*ITUD0D` zWcElGPNLFATYTXvLX!E3?`R@2?;K&Dy_W z@$DPuGX>^vS}L<7B-ZPB^R>@5;+NFz!_qY$UCCOx`>E0|!K~mnQ*Wo7jcVnUT=IT>t?aa@6Kb^QXt2Dgz+I+S8Lf@ + + + + + + + + + + + + + + + + Ingress - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + +
    +
    +
    + + + +
    +
    +
    + + +
    +
    + + + + + + + + +

    Ingress source

    +

    The ingress source creates DNS entries based on Ingress.networking.k8s.io resources.

    +

    Filtering the Ingresses considered

    +

    The --ingress-class flag filters Ingress resources by a set of ingress classes.
    +The flag may be specified multiple times in order to
    +allow multiple ingress classes.

    +

    This source supports the --label-filter flag, which filters Ingress resources
    +by a set of labels.

    +

    Domain names

    +

    The domain names of the DNS entries created from an Ingress are sourced from the following places:

    +
      +
    • Iterates over the Ingress’s spec.rules, adding any non-empty host.
    • +
    +

    This behavior is suppressed if the --ignore-ingress-rules-spec flag was specified
    +or the Ingress had an
    +external-dns.alpha.kubernetes.io/ingress-hostname-source: annotation-only annotation.

    +
      +
    • Iterates over the Ingress’s spec.tls, adding each member of hosts.
    • +
    +

    This behavior is suppressed if the --ignore-ingress-tls-spec flag was specified
    +or the Ingress had an
    +external-dns.alpha.kubernetes.io/ingress-hostname-source: annotation-only annotation,

    +
      +
    • Adds the hostnames from any external-dns.alpha.kubernetes.io/hostname annotation.
    • +
    +

    This behavior is suppressed if the --ignore-hostname-annotation flag was specified
    +or the Ingress had an
    +external-dns.alpha.kubernetes.io/ingress-hostname-source: defined-hosts-only annotation.

    +
      +
    • If no endpoints were produced for an Ingress by the previous steps
      +or the --combine-fqdn-annotation flag was specified, then adds hostnames
      +generated from any--fqdn-template flag.
    • +
    +

    Targets

    +

    The targets of the DNS entries created from an Ingress are sourced from the following places:

    +
      +
    • +

      If the Ingress has an external-dns.alpha.kubernetes.io/target annotation, uses
      +the values from that.

      +
    • +
    • +

      Otherwise, iterates over the Ingress’s status.loadBalancer.ingress,
      +adding each non-empty ip and hostname.

      +
    • +
    + +
    +
    + + + Last update: + July 5, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/sources/sources/index.html b/v0.13.6/sources/sources/index.html new file mode 100644 index 0000000000..8493479df9 --- /dev/null +++ b/v0.13.6/sources/sources/index.html @@ -0,0 +1,2076 @@ + + + + + + + + + + + + + + + + + + About - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + +
    +
    +
    + + + +
    +
    +
    + + +
    +
    + + + + + + + + +

    Sources

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    SourceResourcesannotation-filterlabel-filter
    ambassador-hostHost.getambassador.io
    connector
    contour-httpproxyHttpProxy.projectcontour.ioYes
    cloudfoundry
    crdDNSEndpoint.externaldns.k8s.ioYesYes
    f5-virtualserverVirtualServer.cis.f5.comYes
    gateway-grpcrouteGRPCRoute.gateway.networking.k8s.ioYesYes
    gateway-httprouteHTTPRoute.gateway.networking.k8s.ioYesYes
    gateway-tcprouteTCPRoute.gateway.networking.k8s.ioYesYes
    gateway-tlsrouteTLSRoute.gateway.networking.k8s.ioYesYes
    gateway-udprouteUDPRoute.gateway.networking.k8s.ioYesYes
    gloo-proxyProxy.gloo.solo.io
    ingressIngress.networking.k8s.ioYesYes
    istio-gatewayGateway.networking.istio.ioYes
    istio-virtualserviceVirtualService.networking.istio.ioYes
    kong-tcpingressTCPIngress.configuration.konghq.comYes
    nodeNodeYes
    openshift-routeRoute.route.openshift.ioYesYes
    podPod
    serviceServiceYesYes
    skipper-routegroupRouteGroup.zalando.orgYes
    traefik-proxyIngressRoute.traefik.io IngressRouteTCP.traefik.io IngressRouteUDP.traefik.ioYes
    + +
    +
    + + + Last update: + July 5, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/ttl/index.html b/v0.13.6/ttl/index.html new file mode 100644 index 0000000000..400d60de12 --- /dev/null +++ b/v0.13.6/ttl/index.html @@ -0,0 +1,1999 @@ + + + + + + + + + + + + + + + + + + TTL - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + +
    +
    +
    + + + +
    +
    +
    + + +
    +
    + + + + + + + + +

    Configure DNS record TTL (Time-To-Live)

    +

    An optional annotation external-dns.alpha.kubernetes.io/ttl is available to customize the TTL value of a DNS record.
    +TTL is specified as an integer encoded as string representing seconds.

    +

    To configure it, simply annotate a service/ingress, e.g.:

    +
    apiVersion: v1
    +kind: Service
    +metadata:
    +  annotations:
    +    external-dns.alpha.kubernetes.io/hostname: nginx.external-dns-test.my-org.com.
    +    external-dns.alpha.kubernetes.io/ttl: "60"
    +  ...
    +
    +

    TTL can also be specified as a duration value parsable by Golang time.ParseDuration:

    +
    apiVersion: v1
    +kind: Service
    +metadata:
    +  annotations:
    +    external-dns.alpha.kubernetes.io/hostname: nginx.external-dns-test.my-org.com.
    +    external-dns.alpha.kubernetes.io/ttl: "1m"
    +  ...
    +
    +

    Both examples result in the same value of 60 seconds TTL.

    +

    TTL must be a positive value.

    +

    Providers

    +
      +
    • AWS (Route53)
    • +
    • Azure
    • +
    • Cloudflare
    • +
    • DigitalOcean
    • +
    • DNSimple
    • +
    • Google
    • +
    • InMemory
    • +
    • Linode
    • +
    • TransIP
    • +
    • RFC2136
    • +
    • Vultr
    • +
    • UltraDNS
    • +
    +

    PRs welcome!

    +

    Notes

    +

    When the external-dns.alpha.kubernetes.io/ttl annotation is not provided, the TTL will default to 0 seconds and endpoint.TTL.isConfigured() will be false.

    +

    AWS Provider

    +

    The AWS Provider overrides the value to 300s when the TTL is 0.
    +This value is a constant in the provider code.

    +

    Azure

    +

    TTL value should be between 1 and 2,147,483,647 seconds.
    +By default it will be 300s.

    +

    CloudFlare Provider

    +

    CloudFlare overrides the value to “auto” when the TTL is 0.

    +

    DigitalOcean Provider

    +

    The DigitalOcean Provider overrides the value to 300s when the TTL is 0.
    +This value is a constant in the provider code.

    +

    DNSimple Provider

    +

    The DNSimple Provider default TTL is used when the TTL is 0. The default TTL is 3600s.

    +

    Google Provider

    +

    Previously with the Google Provider, TTL’s were hard-coded to 300s.
    +For safety, the Google Provider overrides the value to 300s when the TTL is 0.
    +This value is a constant in the provider code.

    +

    For the moment, it is impossible to use a TTL value of 0 with the AWS, DigitalOcean, or Google Providers.
    +This behavior may change in the future.

    +

    Linode Provider

    +

    The Linode Provider default TTL is used when the TTL is 0. The default is 24 hours

    +

    TransIP Provider

    +

    The TransIP Provider minimal TTL is used when the TTL is 0. The minimal TTL is 60s.

    +

    Vultr Provider

    +

    The Vultr provider minimal TTL is used when the TTL is 0. The default is 1 hour.

    +

    UltraDNS

    +

    The UltraDNS provider minimal TTL is used when the TTL is not provided. The default TTL is account level default TTL, if defined, otherwise 24 hours.

    + +
    +
    + + + Last update: + August 31, 2022 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/ANS_Group_SafeDNS/index.html b/v0.13.6/tutorials/ANS_Group_SafeDNS/index.html new file mode 100644 index 0000000000..cbae21ecf3 --- /dev/null +++ b/v0.13.6/tutorials/ANS_Group_SafeDNS/index.html @@ -0,0 +1,2261 @@ + + + + + + + + + + + + + + + + + + Setting up ExternalDNS for Services on ANS Group's SafeDNS - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + + + + +
    +
    + + + + + + + + +

    Setting up ExternalDNS for Services on ANS Group’s SafeDNS

    +

    This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using SafeDNS.

    +

    Make sure to use >=0.11.0 version of ExternalDNS for this tutorial.

    +

    Managing DNS with SafeDNS

    +

    If you want to learn about how to use the SafeDNS service read the following tutorials:
    +To learn more about the use of SafeDNS in general, see the following page:

    +

    ANS Group’s SafeDNS documentation.

    +

    Creating SafeDNS credentials

    +

    Generate a fresh API token for use with ExternalDNS, following the instructions
    +at the ANS Group developer Getting-Started
    +page. You will need to grant read/write access to the SafeDNS API. No access to
    +any other ANS Group service is required.

    +

    The environment variable SAFEDNS_TOKEN must have a value of this token to run
    +ExternalDNS with SafeDNS integration.

    +

    Deploy ExternalDNS

    +

    Connect your kubectl client to the cluster you want to test ExternalDNS with.
    +Then apply one of the following manifests file to deploy ExternalDNS.

    +

    Manifest (for clusters without RBAC enabled)

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      containers:
    +      - name: external-dns
    +        # You will need to check what the latest version is yourself:
    +        # https://github.com/kubernetes-sigs/external-dns/releases
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service # ingress is also possible
    +        # (optional) limit to only example.com domains; change to match the
    +        # zone created above.
    +        - --domain-filter=example.com
    +        - --provider=safedns
    +        env:
    +        - name: SAFEDNS_TOKEN
    +          value: "SAFEDNSTOKENSAFEDNSTOKEN"
    +
    +

    Manifest (for clusters with RBAC enabled)

    +
    apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRole
    +metadata:
    +  name: external-dns
    +rules:
    +- apiGroups: [""]
    +  resources: ["services","endpoints","pods"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: ["extensions","networking.k8s.io"]
    +  resources: ["ingresses"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: [""]
    +  resources: ["nodes"]
    +  verbs: ["list"]
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRoleBinding
    +metadata:
    +  name: external-dns-viewer
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: ClusterRole
    +  name: external-dns
    +subjects:
    +- kind: ServiceAccount
    +  name: external-dns
    +  namespace: default
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      serviceAccountName: external-dns
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service # ingress is also possible
    +        # (optional) limit to only example.com domains; change to match the
    +        # zone created above.
    +        - --domain-filter=example.com
    +        - --provider=safedns
    +        env:
    +        - name: SAFEDNS_TOKEN
    +          value: "SAFEDNSTOKENSAFEDNSTOKEN"
    +
    +

    Deploying an Nginx Service

    +

    Create a service file called ‘nginx.yaml’ with the following contents:

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: nginx
    +spec:
    +  selector:
    +    matchLabels:
    +      app: nginx
    +  template:
    +    metadata:
    +      labels:
    +        app: nginx
    +    spec:
    +      containers:
    +      - image: nginx
    +        name: nginx
    +        ports:
    +        - containerPort: 80
    +---
    +apiVersion: v1
    +kind: Service
    +metadata:
    +  name: nginx
    +  annotations:
    +    external-dns.alpha.kubernetes.io/hostname: my-app.example.com
    +spec:
    +  selector:
    +    app: nginx
    +  type: LoadBalancer
    +  ports:
    +    - protocol: TCP
    +      port: 80
    +      targetPort: 80
    +
    +

    Note the annotation on the service; use a hostname that matches the domain
    +filter specified above.

    +

    ExternalDNS uses this annotation to determine what services should be registered
    +with DNS. Removing the annotation will cause ExternalDNS to remove the
    +corresponding DNS records.

    +

    Create the deployment and service:

    +
    $ kubectl create -f nginx.yaml
    +
    +

    Depending where you run your service it can take a little while for your cloud
    +provider to create an external IP for the service.

    +

    Once the service has an external IP assigned, ExternalDNS will notice the new
    +service IP address and synchronize the SafeDNS records.

    +

    Verifying SafeDNS records

    +

    Check your SafeDNS UI and select
    +the appropriate domain to view the records for your SafeDNS zone.

    +

    This should show the external IP address of the service as the A record for your
    +domain.

    +

    Alternatively, you can perform a DNS lookup for the hostname specified:
    +

    $ dig +short my-app.example.com
    +an.ip.addr.ess
    +

    +

    Cleanup

    +

    Now that we have verified that ExternalDNS will automatically manage SafeDNS
    +records, we can delete the tutorial’s example:

    +
    $ kubectl delete service -f nginx.yaml
    +$ kubectl delete service -f externaldns.yaml
    +
    + +
    +
    + + + Last update: + May 29, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/akamai-edgedns/index.html b/v0.13.6/tutorials/akamai-edgedns/index.html new file mode 100644 index 0000000000..622ad45659 --- /dev/null +++ b/v0.13.6/tutorials/akamai-edgedns/index.html @@ -0,0 +1,2386 @@ + + + + + + + + + + + + + + + + + + Setting up External-DNS for Services on Akamai Edge DNS - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + + + + +
    +
    + + + + + + + + +

    Setting up External-DNS for Services on Akamai Edge DNS

    +

    Prerequisites

    +

    External-DNS v0.8.0 or greater.

    +

    Zones

    +

    External-DNS manages service endpoints in existing DNS zones. The Akamai provider does not add, remove or configure new zones. The Akamai Control Center or Akamai DevOps Tools, Akamai CLI and Akamai Terraform Provider can create and manage Edge DNS zones.

    +

    Akamai Edge DNS Authentication

    +

    The Akamai Edge DNS provider requires valid Akamai Edgegrid API authentication credentials to access zones and manage DNS records.

    +

    Either directly by key or indirectly via a file can set credentials for the provider. The Akamai credential keys and mappings to the Akamai provider utilizing different presentation methods are:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Edgegrid Auth KeyExternal-DNS Cmd Line KeyEnvironment/ConfigMap KeyDescription
    hostakamai-serviceconsumerdomainEXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAINAkamai Edgegrid API server
    access_tokenakamai-access-tokenEXTERNAL_DNS_AKAMAI_ACCESS_TOKENAkamai Edgegrid API access token
    client_tokenakamai-client-tokenEXTERNAL_DNS_AKAMAI_CLIENT_TOKENAkamai Edgegrid API client token
    client-secretakamai-client-secretEXTERNAL_DNS_AKAMAI_CLIENT_SECRETAkamai Edgegrid API client secret
    +

    In addition to specifying auth credentials individually, an Akamai Edgegrid .edgerc file convention can set credentials.

    + + + + + + + + + + + + + + + + + + + + +
    External-DNS Cmd LineEnvironment/ConfigMapDescription
    akamai-edgerc-pathEXTERNAL_DNS_AKAMAI_EDGERC_PATHAccessible path to Edgegrid credentials file, e.g /home/test/.edgerc
    akamai-edgerc-sectionEXTERNAL_DNS_AKAMAI_EDGERC_SECTIONSection in Edgegrid credentials file containing credentials
    +

    Akamai API Authentication provides an overview and further information about authorization credentials for API base applications and tools.

    +

    Deploy External-DNS

    +

    An operational External-DNS deployment consists of an External-DNS container and service. The following sections demonstrate the ConfigMap objects that would make up an example functional external DNS kubernetes configuration utilizing NGINX as the service.

    +

    Connect your kubectl client to the External-DNS cluster, and then apply one of the following manifest files:

    +

    Manifest (for clusters without RBAC enabled)

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      serviceAccountName: external-dns
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service  # or ingress or both
    +        - --provider=akamai
    +        - --domain-filter=example.com
    +        # zone-id-filter may be specified as well to filter on contract ID
    +        - --registry=txt
    +        - --txt-owner-id={{ owner-id-for-this-external-dns }}
    +        - --txt-prefix={{ prefix label for TXT record }}.
    +        env:
    +        - name: EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN
    +          valueFrom:
    +            secretKeyRef:
    +              name: external-dns
    +              key: EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN
    +        - name: EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN
    +          valueFrom:
    +            secretKeyRef:
    +              name: external-dns
    +              key: EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN
    +        - name: EXTERNAL_DNS_AKAMAI_CLIENT_SECRET
    +          valueFrom:
    +            secretKeyRef:
    +              name: external-dns
    +              key: EXTERNAL_DNS_AKAMAI_CLIENT_SECRET
    +        - name: EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN
    +          valueFrom:
    +            secretKeyRef:
    +              name: external-dns
    +              key: EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN
    +
    +

    Manifest (for clusters with RBAC enabled)

    +
    apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRole
    +metadata:
    +  name: external-dns
    +rules:
    +- apiGroups: [""]
    +  resources: ["services","endpoints","pods"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: ["extensions","networking.k8s.io"]
    +  resources: ["ingresses"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: [""]
    +  resources: ["nodes"]
    +  verbs: ["watch", "list"]
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRoleBinding
    +metadata:
    +  name: external-dns-viewer
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: ClusterRole
    +  name: external-dns
    +subjects:
    +- kind: ServiceAccount
    +  name: external-dns
    +  namespace: default
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      serviceAccountName: external-dns
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service  # or ingress or both
    +        - --provider=akamai
    +        - --domain-filter=example.com
    +        # zone-id-filter may be specified as well to filter on contract ID
    +        - --registry=txt
    +        - --txt-owner-id={{ owner-id-for-this-external-dns }}
    +        - --txt-prefix={{ prefix label for TXT record }}.
    +        env:
    +        - name: EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN
    +          valueFrom:
    +            secretKeyRef:
    +              name: external-dns
    +              key: EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN
    +        - name: EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN
    +          valueFrom:
    +            secretKeyRef:
    +              name: external-dns
    +              key: EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN
    +        - name: EXTERNAL_DNS_AKAMAI_CLIENT_SECRET
    +          valueFrom:
    +            secretKeyRef:
    +              name: external-dns
    +              key: EXTERNAL_DNS_AKAMAI_CLIENT_SECRET
    +        - name: EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN
    +          valueFrom:
    +            secretKeyRef:
    +              name: external-dns
    +              key: EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN
    +
    +

    Create the deployment for External-DNS:

    +
    $ kubectl apply -f externaldns.yaml
    +
    +

    Deploying an Nginx Service

    +

    Create a service file called ‘nginx.yaml’ with the following contents:

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: nginx
    +spec:
    +  selector:
    +    matchLabels:
    +      app: nginx
    +  template:
    +    metadata:
    +      labels:
    +        app: nginx
    +    spec:
    +      containers:
    +      - image: nginx
    +        name: nginx
    +        ports:
    +        - containerPort: 80
    +---
    +apiVersion: v1
    +kind: Service
    +metadata:
    +  name: nginx
    +  annotations:
    +    external-dns.alpha.kubernetes.io/hostname: nginx.example.com
    +    external-dns.alpha.kubernetes.io/ttl: "600" #optional
    +spec:
    +  selector:
    +    app: nginx
    +  type: LoadBalancer
    +  ports:
    +    - protocol: TCP
    +      port: 80
    +      targetPort: 80
    +
    +

    Create the deployment and service object:

    +
    $ kubectl apply -f nginx.yaml
    +
    +

    Verify Akamai Edge DNS Records

    +

    Wait 3-5 minutes before validating the records to allow the record changes to propagate to all the Akamai name servers.

    +

    Validate records using the Akamai Control Center or by executing a dig, nslookup or similar DNS command.

    +

    Cleanup

    +

    Once you successfully configure and verify record management via External-DNS, you can delete the tutorial’s examples:

    +
    $ kubectl delete -f nginx.yaml
    +$ kubectl delete -f externaldns.yaml
    +
    +

    Additional Information

    +
      +
    • The Akamai provider allows the administrative user to filter zones by both name (domain-filter) and contract Id (zone-id-filter). The Edge DNS API will return a ‘500 Internal Error’ for invalid contract Ids.
    • +
    • The provider will substitute quotes in TXT records with a ` (back tick) when writing records with the API.
    • +
    + +
    +
    + + + Last update: + May 29, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/alibabacloud/index.html b/v0.13.6/tutorials/alibabacloud/index.html new file mode 100644 index 0000000000..b7d6780f48 --- /dev/null +++ b/v0.13.6/tutorials/alibabacloud/index.html @@ -0,0 +1,2476 @@ + + + + + + + + + + + + + + + + + + Setting up ExternalDNS for Services on Alibaba Cloud - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + + + + +
    +
    + + + + + + + + +

    Setting up ExternalDNS for Services on Alibaba Cloud

    +

    This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster on Alibaba Cloud. Make sure to use >=0.5.6 version of ExternalDNS for this tutorial

    +

    RAM Permissions

    +
    {
    +  "Version": "1",
    +  "Statement": [
    +    {
    +      "Action": "alidns:AddDomainRecord",
    +      "Resource": "*",
    +      "Effect": "Allow"
    +    },
    +    {
    +      "Action": "alidns:DeleteDomainRecord",
    +      "Resource": "*",
    +      "Effect": "Allow"
    +    },
    +    {
    +      "Action": "alidns:UpdateDomainRecord",
    +      "Resource": "*",
    +      "Effect": "Allow"
    +    },
    +    {
    +      "Action": "alidns:DescribeDomainRecords",
    +      "Resource": "*",
    +      "Effect": "Allow"
    +    },
    +    {
    +      "Action": "alidns:DescribeDomains",
    +      "Resource": "*",
    +      "Effect": "Allow"
    +    },
    +    {
    +      "Action": "pvtz:AddZoneRecord",
    +      "Resource": "*",
    +      "Effect": "Allow"
    +    },
    +    {
    +      "Action": "pvtz:DeleteZoneRecord",
    +      "Resource": "*",
    +      "Effect": "Allow"
    +    },
    +    {
    +      "Action": "pvtz:UpdateZoneRecord",
    +      "Resource": "*",
    +      "Effect": "Allow"
    +    },
    +    {
    +      "Action": "pvtz:DescribeZoneRecords",
    +      "Resource": "*",
    +      "Effect": "Allow"
    +    },
    +    {
    +      "Action": "pvtz:DescribeZones",
    +      "Resource": "*",
    +      "Effect": "Allow"
    +    },
    +    {
    +      "Action": "pvtz:DescribeZoneInfo",
    +      "Resource": "*",
    +      "Effect": "Allow"
    +    }
    +  ]
    +}
    +
    +

    When running on Alibaba Cloud, you need to make sure that your nodes (on which External DNS runs) have the RAM instance profile with the above RAM role assigned.

    +

    Set up a Alibaba Cloud DNS service or Private Zone service

    +

    Alibaba Cloud DNS Service is the domain name resolution and management service for public access. It routes access from end-users to the designated web app.
    +Alibaba Cloud Private Zone is the domain name resolution and management service for VPC internal access.

    +

    If you prefer to try-out ExternalDNS in one of the existing domain or zone you can skip this step

    +

    Create a DNS domain which will contain the managed DNS records. For public DNS service, the domain name should be valid and owned by yourself.

    +
    $ aliyun alidns AddDomain --DomainName "external-dns-test.com"
    +
    +

    Make a note of the ID of the hosted zone you just created.

    +
    $ aliyun alidns DescribeDomains --KeyWord="external-dns-test.com" | jq -r '.Domains.Domain[0].DomainId'
    +
    +

    Deploy ExternalDNS

    +

    Connect your kubectl client to the cluster you want to test ExternalDNS with.
    +Then apply one of the following manifests file to deploy ExternalDNS.

    +

    Manifest (for clusters without RBAC enabled)

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service
    +        - --source=ingress
    +        - --domain-filter=external-dns-test.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones
    +        - --provider=alibabacloud
    +        - --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization
    +        - --alibaba-cloud-zone-type=public # only look at public hosted zones (valid values are public, private or no value for both)
    +        - --registry=txt
    +        - --txt-owner-id=my-identifier
    +        volumeMounts:
    +        - mountPath: /usr/share/zoneinfo
    +          name: hostpath
    +      volumes:
    +      - name: hostpath
    +        hostPath:
    +          path: /usr/share/zoneinfo
    +          type: Directory
    +
    +

    Manifest (for clusters with RBAC enabled)

    +
    apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRole
    +metadata:
    +  name: external-dns
    +rules:
    +- apiGroups: [""]
    +  resources: ["services","endpoints","pods"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: ["extensions","networking.k8s.io"]
    +  resources: ["ingresses"] 
    +  verbs: ["get","watch","list"]
    +- apiGroups: [""]
    +  resources: ["nodes"]
    +  verbs: ["list"]
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRoleBinding
    +metadata:
    +  name: external-dns-viewer
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: ClusterRole
    +  name: external-dns
    +subjects:
    +- kind: ServiceAccount
    +  name: external-dns
    +  namespace: default
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      serviceAccountName: external-dns
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service
    +        - --source=ingress
    +        - --domain-filter=external-dns-test.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones
    +        - --provider=alibabacloud
    +        - --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization
    +        - --alibaba-cloud-zone-type=public # only look at public hosted zones (valid values are public, private or no value for both)
    +        - --registry=txt
    +        - --txt-owner-id=my-identifier
    +        - --alibaba-cloud-config-file= # enable sts token 
    +        volumeMounts:
    +        - mountPath: /usr/share/zoneinfo
    +          name: hostpath
    +      volumes:
    +      - name: hostpath
    +        hostPath:
    +          path: /usr/share/zoneinfo
    +          type: Directory
    +
    +

    Arguments

    +

    This list is not the full list, but a few arguments that where chosen.

    +

    alibaba-cloud-zone-type

    +

    alibaba-cloud-zone-type allows filtering for private and public zones

    +
      +
    • If value is public, it will sync with records in Alibaba Cloud DNS Service
    • +
    • If value is private, it will sync with records in Alibaba Cloud Private Zone Service
    • +
    +

    Verify ExternalDNS works (Ingress example)

    +

    Create an ingress resource manifest file.

    +
    +

    For ingress objects ExternalDNS will create a DNS record based on the host specified for the ingress object.

    +
    +
    apiVersion: networking.k8s.io/v1
    +kind: Ingress
    +metadata:
    +  name: foo
    +spec:
    +  ingressClassName: nginx # use the one that corresponds to your ingress controller.
    +  rules:
    +  - host: foo.external-dns-test.com
    +    http:
    +      paths:
    +      - backend:
    +          service:
    +            name: foo
    +            port:
    +              number: 80
    +        pathType: Prefix
    +
    +

    Verify ExternalDNS works (Service example)

    +

    Create the following sample application to test that ExternalDNS works.

    +
    +

    For services ExternalDNS will look for the annotation external-dns.alpha.kubernetes.io/hostname on the service and use the corresponding value.

    +
    +
    apiVersion: v1
    +kind: Service
    +metadata:
    +  name: nginx
    +  annotations:
    +    external-dns.alpha.kubernetes.io/hostname: nginx.external-dns-test.com.
    +spec:
    +  type: LoadBalancer
    +  ports:
    +  - port: 80
    +    name: http
    +    targetPort: 80
    +  selector:
    +    app: nginx
    +
    +---
    +
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: nginx
    +spec:
    +  selector:
    +    matchLabels:
    +      app: nginx
    +  template:
    +    metadata:
    +      labels:
    +        app: nginx
    +    spec:
    +      containers:
    +      - image: nginx
    +        name: nginx
    +        ports:
    +        - containerPort: 80
    +          name: http
    +
    +

    After roughly two minutes check that a corresponding DNS record for your service was created.

    +
    $ aliyun alidns DescribeDomainRecords --DomainName=external-dns-test.com
    +{
    +  "PageNumber": 1,
    +  "TotalCount": 1,
    +  "PageSize": 20,
    +  "RequestId": "1DBEF426-F771-46C7-9802-4989E9C94EE8",
    +  "DomainRecords": {
    +    "Record": [
    +      {
    +        "RR": "nginx",
    +        "Status": "ENABLE",
    +        "Value": "1.2.3.4",
    +        "Weight": 1,
    +        "RecordId": "3994015629411328",
    +        "Type": "A",
    +        "DomainName": "external-dns-test.com",
    +        "Locked": false,
    +        "Line": "default",
    +        "TTL": 600
    +      },
    +      {
    +        "RR": "nginx",
    +        "Status": "ENABLE",
    +        "Value": "heritage=external-dns;external-dns/owner=my-identifier",
    +        "Weight": 1,
    +        "RecordId": "3994015629411329",
    +        "Type": "TTL",
    +        "DomainName": "external-dns-test.com",
    +        "Locked": false,
    +        "Line": "default",
    +        "TTL": 600
    +      }      
    +    ]
    +  }
    +}
    +
    +

    Note created TXT record alongside ALIAS record. TXT record signifies that the corresponding ALIAS record is managed by ExternalDNS. This makes ExternalDNS safe for running in environments where there are other records managed via other means.

    +

    Let’s check that we can resolve this DNS name. We’ll ask the nameservers assigned to your zone first.

    +
    $ dig nginx.external-dns-test.com.
    +
    +

    If you hooked up your DNS zone with its parent zone correctly you can use curl to access your site.

    +
    $ curl nginx.external-dns-test.com.
    +<!DOCTYPE html>
    +<html>
    +<head>
    +<title>Welcome to nginx!</title>
    +...
    +</head>
    +<body>
    +...
    +</body>
    +</html>
    +
    +

    Custom TTL

    +

    The default DNS record TTL (Time-To-Live) is 300 seconds. You can customize this value by setting the annotation external-dns.alpha.kubernetes.io/ttl.
    +e.g., modify the service manifest YAML file above:

    +
    apiVersion: v1
    +kind: Service
    +metadata:
    +  name: nginx
    +  annotations:
    +    external-dns.alpha.kubernetes.io/hostname: nginx.external-dns-test.com
    +    external-dns.alpha.kubernetes.io/ttl: 60
    +spec:
    +    ...
    +
    +

    This will set the DNS record’s TTL to 60 seconds.

    +

    Clean up

    +

    Make sure to delete all Service objects before terminating the cluster so all load balancers get cleaned up correctly.

    +
    $ kubectl delete service nginx
    +
    +

    Give ExternalDNS some time to clean up the DNS records for you. Then delete the hosted zone if you created one for the testing purpose.

    +
    $ aliyun alidns DeleteDomain --DomainName external-dns-test.com
    +
    +

    For more info about Alibaba Cloud external dns, please refer this docs

    + +
    +
    + + + Last update: + May 29, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/aws-load-balancer-controller/index.html b/v0.13.6/tutorials/aws-load-balancer-controller/index.html new file mode 100644 index 0000000000..e9a33a4f6e --- /dev/null +++ b/v0.13.6/tutorials/aws-load-balancer-controller/index.html @@ -0,0 +1,2162 @@ + + + + + + + + + + + + + + + + + + Using ExternalDNS with aws-load-balancer-controller - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + +
    +
    +
    + + + +
    +
    +
    + + +
    +
    + + + + + + + + +

    Using ExternalDNS with aws-load-balancer-controller

    +

    This tutorial describes how to use ExternalDNS with the aws-load-balancer-controller.

    +

    Setting up ExternalDNS and aws-load-balancer-controller

    +

    Follow the AWS tutorial to setup ExternalDNS for use in Kubernetes clusters
    +running in AWS. Specify the source=ingress argument so that ExternalDNS will look
    +for hostnames in Ingress objects. In addition, you may wish to limit which Ingress
    +objects are used as an ExternalDNS source via the ingress-class argument, but
    +this is not required.

    +

    For help setting up the AWS Load Balancer Controller, follow the Setup Guide.

    +

    Note that the AWS Load Balancer Controller uses the same tags for subnet auto-discovery
    +as Kubernetes does with the AWS cloud provider.

    +

    In the examples that follow, it is assumed that you configured the ALB Ingress
    +Controller with the ingress-class=alb argument (not to be confused with the
    +same argument to ExternalDNS) so that the controller will only respect Ingress
    +objects with the ingressClassName field set to “alb”.

    +

    Deploy an example application

    +

    Create the following sample “echoserver” application to demonstrate how
    +ExternalDNS works with ALB ingress objects.

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: echoserver
    +spec:
    +  replicas: 1
    +  selector:
    +    matchLabels:
    +      app: echoserver
    +  template:
    +    metadata:
    +      labels:
    +        app: echoserver
    +    spec:
    +      containers:
    +      - image: gcr.io/google_containers/echoserver:1.4
    +        imagePullPolicy: Always
    +        name: echoserver
    +        ports:
    +        - containerPort: 8080
    +---
    +apiVersion: v1
    +kind: Service
    +metadata:
    +  name: echoserver
    +spec:
    +  ports:
    +    - port: 80
    +      targetPort: 8080
    +      protocol: TCP
    +  type: NodePort
    +  selector:
    +    app: echoserver
    +
    +

    Note that the Service object is of type NodePort. We don’t need a Service of
    +type LoadBalancer here, since we will be using an Ingress to create an ALB.

    +

    Ingress examples

    +

    Create the following Ingress to expose the echoserver application to the Internet.

    +
    apiVersion: networking.k8s.io/v1
    +kind: Ingress
    +metadata:
    +  annotations:
    +    alb.ingress.kubernetes.io/scheme: internet-facing
    +  name: echoserver
    +spec:
    +  ingressClassName: alb
    +  rules:
    +  - host: echoserver.mycluster.example.org
    +    http: &echoserver_root
    +      paths:
    +      - path: /
    +        backend:
    +          service:
    +            name: echoserver
    +            port:
    +              number: 80
    +        pathType: Prefix
    +  - host: echoserver.example.org
    +    http: *echoserver_root
    +
    +

    The above should result in the creation of an (ipv4) ALB in AWS which will forward
    +traffic to the echoserver application.

    +

    If the source=ingress argument is specified, then ExternalDNS will create DNS
    +records based on the hosts specified in ingress objects. The above example would
    +result in two alias records being created, echoserver.mycluster.example.org and
    +echoserver.example.org, which both alias the ALB that is associated with the
    +Ingress object.

    +

    Note that the above example makes use of the YAML anchor feature to avoid having
    +to repeat the http section for multiple hosts that use the exact same paths. If
    +this Ingress object will only be fronting one backend Service, we might instead
    +create the following:

    +
    apiVersion: networking.k8s.io/v1
    +kind: Ingress
    +metadata:
    +  annotations:
    +    alb.ingress.kubernetes.io/scheme: internet-facing
    +    external-dns.alpha.kubernetes.io/hostname: echoserver.mycluster.example.org, echoserver.example.org
    +  name: echoserver
    +spec:
    +  ingressClassName: alb
    +  rules:
    +  - http:
    +      paths:
    +      - path: /
    +        backend:
    +          service:
    +            name: echoserver
    +            port:
    +              number: 80
    +        pathType: Prefix
    +
    +

    In the above example we create a default path that works for any hostname, and
    +make use of the external-dns.alpha.kubernetes.io/hostname annotation to create
    +multiple aliases for the resulting ALB.

    +

    Dualstack ALBs

    +

    AWS supports both IPv4 and “dualstack” (both IPv4 and IPv6) interfaces for ALBs.
    +The AWS Load Balancer Controller uses the alb.ingress.kubernetes.io/ip-address-type
    +annotation (which defaults to ipv4) to determine this. If this annotation is
    +set to dualstack then ExternalDNS will create two alias records (one A record
    +and one AAAA record) for each hostname associated with the Ingress object.

    +

    Example:

    +
    apiVersion: networking.k8s.io/v1
    +kind: Ingress
    +metadata:
    +  annotations:
    +    alb.ingress.kubernetes.io/scheme: internet-facing
    +    alb.ingress.kubernetes.io/ip-address-type: dualstack
    +  name: echoserver
    +spec:
    +  ingressClassName: alb
    +  rules:
    +  - host: echoserver.example.org
    +    http:
    +      paths:
    +      - path: /
    +        backend:
    +          service:
    +            name: echoserver
    +            port:
    +              number: 80
    +        pathType: Prefix
    +
    +

    The above Ingress object will result in the creation of an ALB with a dualstack
    +interface. ExternalDNS will create both an A echoserver.example.org record and
    +an AAAA record of the same name, that each are aliases for the same ALB.

    + +
    +
    + + + Last update: + May 5, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/aws-sd/index.html b/v0.13.6/tutorials/aws-sd/index.html new file mode 100644 index 0000000000..b6bcf19956 --- /dev/null +++ b/v0.13.6/tutorials/aws-sd/index.html @@ -0,0 +1,2318 @@ + + + + + + + + + + + + + + + + + + Setting up ExternalDNS using AWS Cloud Map API - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + + + + +
    +
    + + + + + + + + +

    Setting up ExternalDNS using AWS Cloud Map API

    +

    This tutorial describes how to set up ExternalDNS for usage within a Kubernetes cluster with AWS Cloud Map API.

    +

    AWS Cloud Map API is an alternative approach to managing DNS records directly using the Route53 API. It is more suitable for a dynamic environment where service endpoints change frequently. It abstracts away technical details of the DNS protocol and offers a simplified model. AWS Cloud Map consists of three main API calls:

    +
      +
    • CreatePublicDnsNamespace – automatically creates a DNS hosted zone
    • +
    • CreateService – creates a new named service inside the specified namespace
    • +
    • RegisterInstance/DeregisterInstance – can be called multiple times to create a DNS record for the specified Service
    • +
    +

    Learn more about the API in the AWS Cloud Map API Reference.

    +

    IAM Permissions

    +

    To use the AWS Cloud Map API, a user must have permissions to create the DNS namespace. Additionally you need to make sure that your nodes (on which External DNS runs) have an IAM instance profile with the AWSCloudMapFullAccess managed policy attached, that provides following permissions:

    +
    {
    +  "Version": "2012-10-17",
    +  "Statement": [
    +    {
    +      "Effect": "Allow",
    +      "Action": [
    +        "route53:GetHostedZone",
    +        "route53:ListHostedZonesByName",
    +        "route53:CreateHostedZone",
    +        "route53:DeleteHostedZone",
    +        "route53:ChangeResourceRecordSets",
    +        "route53:CreateHealthCheck",
    +        "route53:GetHealthCheck",
    +        "route53:DeleteHealthCheck",
    +        "route53:UpdateHealthCheck",
    +        "ec2:DescribeVpcs",
    +        "ec2:DescribeRegions",
    +        "servicediscovery:*"
    +      ],
    +      "Resource": [
    +        "*"
    +      ]
    +    }
    +  ]
    +}
    +
    +

    Set up a namespace

    +

    Create a DNS namespace using the AWS Cloud Map API:

    +
    $ aws servicediscovery create-public-dns-namespace --name "external-dns-test.my-org.com"
    +
    +

    Verify that the namespace was truly created

    +
    $ aws servicediscovery list-namespaces
    +
    +

    Deploy ExternalDNS

    +

    Connect your kubectl client to the cluster that you want to test ExternalDNS with.
    +Then apply the following manifest file to deploy ExternalDNS.

    +

    Manifest (for clusters without RBAC enabled)

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        env:
    +          - name: AWS_REGION
    +            value: us-east-1 # put your CloudMap NameSpace region
    +        args:
    +        - --source=service
    +        - --source=ingress
    +        - --domain-filter=external-dns-test.my-org.com # Makes ExternalDNS see only the namespaces that match the specified domain. Omit the filter if you want to process all available namespaces.
    +        - --provider=aws-sd
    +        - --aws-zone-type=public # Only look at public namespaces. Valid values are public, private, or no value for both)
    +        - --txt-owner-id=my-identifier
    +
    +

    Manifest (for clusters with RBAC enabled)

    +
    apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRole
    +metadata:
    +  name: external-dns
    +rules:
    +- apiGroups: [""]
    +  resources: ["services","endpoints","pods"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: ["extensions","networking.k8s.io"]
    +  resources: ["ingresses"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: [""]
    +  resources: ["nodes"]
    +  verbs: ["list","watch"]
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRoleBinding
    +metadata:
    +  name: external-dns-viewer
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: ClusterRole
    +  name: external-dns
    +subjects:
    +- kind: ServiceAccount
    +  name: external-dns
    +  namespace: default
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      serviceAccountName: external-dns
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        env:
    +          - name: AWS_REGION
    +            value: us-east-1 # put your CloudMap NameSpace region
    +        args:
    +        - --source=service
    +        - --source=ingress
    +        - --domain-filter=external-dns-test.my-org.com # Makes ExternalDNS see only the namespaces that match the specified domain. Omit the filter if you want to process all available namespaces.
    +        - --provider=aws-sd
    +        - --aws-zone-type=public # Only look at public namespaces. Valid values are public, private, or no value for both)
    +        - --txt-owner-id=my-identifier
    +
    +

    Verify that ExternalDNS works (Service example)

    +

    Create the following sample application to test that ExternalDNS works.

    +
    +

    For services ExternalDNS will look for the annotation external-dns.alpha.kubernetes.io/hostname on the service and use the corresponding value.

    +
    +
    apiVersion: v1
    +kind: Service
    +metadata:
    +  name: nginx
    +  annotations:
    +    external-dns.alpha.kubernetes.io/hostname: nginx.external-dns-test.my-org.com
    +spec:
    +  type: LoadBalancer
    +  ports:
    +  - port: 80
    +    name: http
    +    targetPort: 80
    +  selector:
    +    app: nginx
    +
    +---
    +
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: nginx
    +spec:
    +  selector:
    +    matchLabels:
    +      app: nginx
    +  template:
    +    metadata:
    +      labels:
    +        app: nginx
    +    spec:
    +      containers:
    +      - image: nginx
    +        name: nginx
    +        ports:
    +        - containerPort: 80
    +          name: http
    +
    +

    After one minute check that a corresponding DNS record for your service was created in your hosted zone. We recommended that you use the Amazon Route53 console for that purpose.

    +

    Custom TTL

    +

    The default DNS record TTL (time to live) is 300 seconds. You can customize this value by setting the annotation external-dns.alpha.kubernetes.io/ttl.
    +For example, modify the service manifest YAML file above:

    +
    apiVersion: v1
    +kind: Service
    +metadata:
    +  name: nginx
    +  annotations:
    +    external-dns.alpha.kubernetes.io/hostname: nginx.external-dns-test.my-org.com
    +    external-dns.alpha.kubernetes.io/ttl: 60
    +spec:
    +    ...
    +
    +

    This will set the TTL for the DNS record to 60 seconds.

    +

    Clean up

    +

    Delete all service objects before terminating the cluster so all load balancers get cleaned up correctly.

    +
    $ kubectl delete service nginx
    +
    +

    Give ExternalDNS some time to clean up the DNS records for you. Then delete the remaining service and namespace.

    +
    $ aws servicediscovery list-services
    +
    +{
    +    "Services": [
    +        {
    +            "Id": "srv-6dygt5ywvyzvi3an",
    +            "Arn": "arn:aws:servicediscovery:us-west-2:861574988794:service/srv-6dygt5ywvyzvi3an",
    +            "Name": "nginx"
    +        }
    +    ]
    +}
    +
    +
    $ aws servicediscovery delete-service --id srv-6dygt5ywvyzvi3an
    +
    +
    $ aws servicediscovery list-namespaces
    +{
    +    "Namespaces": [
    +        {
    +            "Type": "DNS_PUBLIC",
    +            "Id": "ns-durf2oxu4gxcgo6z",
    +            "Arn": "arn:aws:servicediscovery:us-west-2:861574988794:namespace/ns-durf2oxu4gxcgo6z",
    +            "Name": "external-dns-test.my-org.com"
    +        }
    +    ]
    +}
    +
    +
    $ aws servicediscovery delete-namespace --id ns-durf2oxu4gxcgo6z
    +
    + +
    +
    + + + Last update: + May 29, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/aws/index.html b/v0.13.6/tutorials/aws/index.html new file mode 100644 index 0000000000..a5cc8be5fc --- /dev/null +++ b/v0.13.6/tutorials/aws/index.html @@ -0,0 +1,3394 @@ + + + + + + + + + + + + + + + + + + Setting up ExternalDNS for Services on AWS - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + +
    +
    +
    + + + +
    +
    +
    + + +
    +
    + + + + + + + + +

    Setting up ExternalDNS for Services on AWS

    +

    This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster on AWS. Make sure to use >=0.11.0 version of ExternalDNS for this tutorial

    +

    IAM Policy

    +

    The following IAM Policy document allows ExternalDNS to update Route53 Resource
    +Record Sets and Hosted Zones. You’ll want to create this Policy in IAM first. In
    +our example, we’ll call the policy AllowExternalDNSUpdates (but you can call
    +it whatever you prefer).

    +

    If you prefer, you may fine-tune the policy to permit updates only to explicit
    +Hosted Zone IDs.

    +
    {
    +  "Version": "2012-10-17",
    +  "Statement": [
    +    {
    +      "Effect": "Allow",
    +      "Action": [
    +        "route53:ChangeResourceRecordSets"
    +      ],
    +      "Resource": [
    +        "arn:aws:route53:::hostedzone/*"
    +      ]
    +    },
    +    {
    +      "Effect": "Allow",
    +      "Action": [
    +        "route53:ListHostedZones",
    +        "route53:ListResourceRecordSets",
    +        "route53:ListTagsForResource"
    +      ],
    +      "Resource": [
    +        "*"
    +      ]
    +    }
    +  ]
    +}
    +
    +

    If you are using the AWS CLI, you can run the following to install the above policy (saved as policy.json). This can be use in subsequent steps to allow ExternalDNS to access Route53 zones.

    +
    aws iam create-policy --policy-name "AllowExternalDNSUpdates" --policy-document file://policy.json
    +
    +# example: arn:aws:iam::XXXXXXXXXXXX:policy/AllowExternalDNSUpdates
    +export POLICY_ARN=$(aws iam list-policies \
    + --query 'Policies[?PolicyName==`AllowExternalDNSUpdates`].Arn' --output text)
    +
    +

    Provisioning a Kubernetes cluster

    +

    You can use eksctl to easily provision an Amazon Elastic Kubernetes Service (EKS) cluster that is suitable for this tutorial. See Getting started with Amazon EKS – eksctl.

    +
    export EKS_CLUSTER_NAME="my-externaldns-cluster"
    +export EKS_CLUSTER_REGION="us-east-2"
    +export KUBECONFIG="$HOME/.kube/${EKS_CLUSTER_NAME}-${EKS_CLUSTER_REGION}.yaml"
    +
    +eksctl create cluster --name $EKS_CLUSTER_NAME --region $EKS_CLUSTER_REGION
    +
    +

    Feel free to use other provisioning tools or an existing cluster. If Terraform is used, vpc and eks modules are recommended for standing up an EKS cluster. Amazon has a workshop called Amazon EKS Terraform Workshop that may be useful for this process.

    +

    Permissions to modify DNS zone

    +

    You will need to use the above policy (represented by the POLICY_ARN environment variable) to allow ExternalDNS to update records in Route53 DNS zones. Here are three common ways this can be accomplished:

    + +

    For this tutorial, ExternalDNS will use the environment variable EXTERNALDNS_NS to represent the namespace, defaulted to default. Feel free to change this to something else, such externaldns or kube-addons. Make sure to edit the subjects[0].namespace for the ClusterRoleBinding resource when deploying ExternalDNS with RBAC enabled. See Manifest (for clusters with RBAC enabled) for more information.

    +

    Additionally, throughout this tutorial, the example domain of example.com is used. Change this to appropriate domain under your control. See Set up a hosted zone section.

    +

    Node IAM Role

    +

    In this method, you can attach a policy to the Node IAM Role. This will allow nodes in the Kubernetes cluster to access Route53 zones, which allows ExternalDNS to update DNS records. Given that this allows all containers to access Route53, not just ExternalDNS, running on the node with these privileges, this method is not recommended, and is only suitable for limited limited test environments.

    +

    If you are using eksctl to provision a new cluster, you add the policy at creation time with:

    +
    eksctl create cluster --external-dns-access \
    +  --name $EKS_CLUSTER_NAME --region $EKS_CLUSTER_REGION \
    +
    +

    ⚠ WARNING: This will assign allow read-write access to all nodes in the cluster, not just ExternalDNS. For this reason, this method is only suitable for limited test environments.

    +

    If you already provisioned a cluster or use other provisioning tools like Terraform, you can use AWS CLI to attach the policy to the Node IAM Role.

    +

    Get the Node IAM role name

    +

    The role name of the role associated with the node(s) where ExternalDNS will run is needed. An easy way to get the role name is to use the AWS web console (https://console.aws.amazon.com/eks/), and find any instance in the target node group and copy the role name associated with that instance.

    +
    Get role name with a single managed nodegroup
    +

    From the command line, if you have a single managed node group, the default with eksctl create cluster, you can find the role name with the following:

    +
    # get managed node group name (assuming there's only one node group)
    +GROUP_NAME=$(aws eks list-nodegroups --cluster-name $EKS_CLUSTER_NAME \
    +  --query nodegroups --out text)
    +# fetch role arn given node group name
    +ROLE_ARN=$(aws eks describe-nodegroup --cluster-name $EKS_CLUSTER_NAME \
    +  --nodegroup-name $GROUP_NAME --query nodegroup.nodeRole --out text)
    +# extract just the name part of role arn
    +ROLE_NAME=${NODE_ROLE_ARN##*/}
    +
    +
    Get role name with other configurations
    +

    If you have multiple node groups or any unmanaged node groups, the process gets more complex. The first step is to get the instance host name of the desired node to where ExternalDNS will be deployed or is already deployed:

    +
    # node instance name of one of the external dns pods currently running
    +INSTANCE_NAME=$(kubectl get pods --all-namespaces \
    +  --selector app.kubernetes.io/instance=external-dns \
    +  --output jsonpath='{.items[0].spec.nodeName}')
    +
    +# instance name of one of the nodes (change if node group is different)
    +INSTANCE_NAME=$(kubectl get nodes --output name | cut -d'/' -f2 | tail -1)
    +
    +

    With the instance host name, you can then get the instance id:

    +
    get_instance_id() {
    +  INSTANCE_NAME=$1 # example: ip-192-168-74-34.us-east-2.compute.internal
    +
    +  # get list of nodes
    +  # ip-192-168-74-34.us-east-2.compute.internal aws:///us-east-2a/i-xxxxxxxxxxxxxxxxx
    +  # ip-192-168-86-105.us-east-2.compute.internal    aws:///us-east-2a/i-xxxxxxxxxxxxxxxxx
    +  NODES=$(kubectl get nodes \
    +   --output jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.providerID}{"\n"}{end}')
    +
    +  # print instance id from matching node
    +  grep $INSTANCE_NAME <<< "$NODES" | cut -d'/' -f5
    +}
    +
    +INSTANCE_ID=$(get_instance_id $INSTANCE_NAME)
    +
    +

    With the instance id, you can get the associated role name:

    +
    findRoleName() {
    +  INSTANCE_ID=$1
    +
    +  # get all of the roles
    +  ROLES=($(aws iam list-roles --query Roles[*].RoleName --out text))
    +  for ROLE in ${ROLES[*]}; do
    +    # get instance profile arn
    +    PROFILE_ARN=$(aws iam list-instance-profiles-for-role \
    +      --role-name $ROLE --query InstanceProfiles[0].Arn --output text)
    +    # if there is an instance profile
    +    if [[ "$PROFILE_ARN" != "None" ]]; then
    +      # get all the instances with this associated instance profile
    +      INSTANCES=$(aws ec2 describe-instances \
    +        --filters Name=iam-instance-profile.arn,Values=$PROFILE_ARN \
    +        --query Reservations[*].Instances[0].InstanceId --out text)
    +      # find instances that match the instant profile
    +      for INSTANCE in ${INSTANCES[*]}; do
    +        # set role name value if there is a match
    +        if [[ "$INSTANCE_ID" == "$INSTANCE" ]]; then ROLE_NAME=$ROLE; fi
    +      done
    +    fi
    +  done
    +
    +  echo $ROLE_NAME
    +}
    +
    +NODE_ROLE_NAME=$(findRoleName $INSTANCE_ID)
    +
    +

    Using the role name, you can associate the policy that was created earlier:

    +
    # attach policy arn created earlier to node IAM role
    +aws iam attach-role-policy --role-name $NODE_ROLE_NAME --policy-arn $POLICY_ARN
    +
    +

    ⚠ WARNING: This will assign allow read-write access to all pods running on the same node pool, not just the ExternalDNS pod(s).

    +

    Deploy ExternalDNS with attached policy to Node IAM Role

    +

    If ExternalDNS is not yet deployed, follow the steps under Deploy ExternalDNS using either RBAC or non-RBAC.

    +

    NOTE: Before deleting the cluster during, be sure to run aws iam detach-role-policy. Otherwise, there can be errors as the provisioning system, such as eksctl or terraform, will not be able to delete the roles with the attached policy.

    +

    Static credentials

    +

    In this method, the policy is attached to an IAM user, and the credentials secrets for the IAM user are then made available using a Kubernetes secret.

    +

    This method is not the preferred method as the secrets in the credential file could be copied and used by an unauthorized threat actor. However, if the Kubernetes cluster is not hosted on AWS, it may be the only method available. Given this situation, it is important to limit the associated privileges to just minimal required privileges, i.e. read-write access to Route53, and not used a credentials file that has extra privileges beyond what is required.

    +

    Create IAM user and attach the policy

    +
    # create IAM user
    +aws iam create-user --user-name "externaldns"
    +
    +# attach policy arn created earlier to IAM user
    +aws iam attach-user-policy --user-name "externaldns" --policy-arn $POLICY_ARN
    +
    +

    Create the static credentials

    +
    SECRET_ACCESS_KEY=$(aws iam create-access-key --user-name "externaldns")
    +cat <<-EOF > /local/path/to/credentials
    +
    +[default]
    +aws_access_key_id = $(echo $SECRET_ACCESS_KEY | jq -r '.AccessKey.AccessKeyId')
    +aws_secret_access_key = $(echo $SECRET_ACCESS_KEY | jq -r '.AccessKey.SecretAccessKey')
    +EOF
    +
    +

    Create Kubernetes secret from credentials

    +
    kubectl create secret generic external-dns \
    +  --namespace ${EXTERNALDNS_NS:-"default"} --from-file /local/path/to/credentials
    +
    +

    Deploy ExternalDNS using static credentials

    +

    Follow the steps under Deploy ExternalDNS using either RBAC or non-RBAC. Make sure to uncomment the section that mounts volumes, so that the credentials can be mounted.

    +

    IAM Roles for Service Accounts

    +

    IRSA (IAM roles for Service Accounts) allows cluster operators to map AWS IAM Roles to Kubernetes Service Accounts. This essentially allows only ExternalDNS pods to access Route53 without exposing any static credentials.

    +

    This is the preferred method as it implements PoLP (Principal of Least Privilege).

    +

    IMPORTANT: This method requires using KSA (Kuberntes service account) and RBAC.

    +

    This method requires deploying with RBAC. See Manifest (for clusters with RBAC enabled) when ready to deploy ExternalDNS.

    +

    NOTE: Similar methods to IRSA on AWS are kiam, which is in maintenence mode, and has instructions for creating an IAM role, and also kube2iam. IRSA is the officially supported method for EKS clusters, and so for non-EKS clusters on AWS, these other tools could be an option.

    +

    Verify OIDC is supported

    +
    aws eks describe-cluster --name $EKS_CLUSTER_NAME \
    +  --query "cluster.identity.oidc.issuer" --output text
    +
    +

    Associate OIDC to cluster

    +

    Configure the cluster with an OIDC provider and add support for IRSA (IAM roles for Service Accounts).

    +

    If you used eksctl to provision the EKS cluster, you can update it with the following command:

    +
    eksctl utils associate-iam-oidc-provider \
    +  --cluster $EKS_CLUSTER_NAME --approve
    +
    +

    If the cluster was provisioned with Terraform, you can use the iam_openid_connect_provider resource (ref) to associate to the OIDC provider.

    +

    Create an IAM role bound to a service account

    +

    For the next steps in this process, we will need to associate the external-dns service account and a role used to grant access to Route53. This requires the following steps:

    +
      +
    1. Create a role with a trust relationship to the cluster’s OIDC provider
    2. +
    3. Attach the AllowExternalDNSUpdates policy to the role
    4. +
    5. Create the external-dns service account
    6. +
    7. Add annotation to the service account with the role arn
    8. +
    +
    Use eksctl with eksctl created EKS cluster
    +

    If eksctl was used to provision the EKS cluster, you can perform all of these steps with the following command:

    +
    eksctl create iamserviceaccount \
    +  --cluster $EKS_CLUSTER_NAME \
    +  --name "external-dns" \
    +  --namespace ${EXTERNALDNS_NS:-"default"} \
    +  --attach-policy-arn $POLICY_ARN \
    +  --approve
    +
    +
    Use aws cli with any EKS cluster
    +

    Otherwise, we can do the following steps using aws commands (also see Creating an IAM role and policy for your service account):

    +
    ACCOUNT_ID=$(aws sts get-caller-identity \
    +  --query "Account" --output text)
    +OIDC_PROVIDER=$(aws eks describe-cluster --name $EKS_CLUSTER_NAME \
    +  --query "cluster.identity.oidc.issuer" --output text | sed -e 's|^https://||')
    +
    +cat <<-EOF > trust.json
    +{
    +    "Version": "2012-10-17",
    +    "Statement": [
    +        {
    +            "Effect": "Allow",
    +            "Principal": {
    +                "Federated": "arn:aws:iam::$ACCOUNT_ID:oidc-provider/$OIDC_PROVIDER"
    +            },
    +            "Action": "sts:AssumeRoleWithWebIdentity",
    +            "Condition": {
    +                "StringEquals": {
    +                    "$OIDC_PROVIDER:sub": "system:serviceaccount:${EXTERNALDNS_NS:-"default"}:external-dns",
    +                    "$OIDC_PROVIDER:aud": "sts.amazonaws.com"
    +                }
    +            }
    +        }
    +    ]
    +}
    +EOF
    +
    +IRSA_ROLE="external-dns-irsa-role"
    +aws iam create-role --role-name $IRSA_ROLE --assume-role-policy-document file://trust.json
    +aws iam attach-role-policy --role-name $IRSA_ROLE --policy-arn $POLICY_ARN
    +
    +ROLE_ARN=$(aws iam get-role --role-name $IRSA_ROLE --query Role.Arn --output text)
    +
    +# Create service account (skip is already created)
    +kubectl create serviceaccount "external-dns" --namespace ${EXTERNALDNS_NS:-"default"}
    +
    +# Add annotation referencing IRSA role
    +kubectl patch serviceaccount "external-dns" --namespace ${EXTERNALDNS_NS:-"default"} --patch \
    + "{\"metadata\": { \"annotations\": { \"eks.amazonaws.com/role-arn\": \"$ROLE_ARN\" }}}"
    +
    +

    If any part of this step is misconfigured, such as the role with incorrect namespace configured in the trust relationship, annotation pointing the the wrong role, etc., you will see errors like WebIdentityErr: failed to retrieve credentials. Check the configuration and make corrections.

    +

    When the service account annotations are updated, then the current running pods will have to be terminated, so that new pod(s) with proper configuration (environment variables) will be created automatically.

    +

    When annotation is added to service account, the ExternalDNS pod(s) scheduled will have AWS_ROLE_ARN, AWS_STS_REGIONAL_ENDPOINTS, and AWS_WEB_IDENTITY_TOKEN_FILE environment variables injected automatically.

    +

    Deploy ExternalDNS using IRSA

    +

    Follow the steps under Manifest (for clusters with RBAC enabled). Make sure to comment out the service account section if this has been created already.

    +

    If you deployed ExternalDNS before adding the service account annotation and the corresponding role, you will likely see error with failed to list hosted zones: AccessDenied: User. You can delete the current running ExternalDNS pod(s) after updating the annotation, so that new pods scheduled will have appropriate configuration to access Route53.

    +

    Set up a hosted zone

    +

    If you prefer to try-out ExternalDNS in one of the existing hosted-zones you can skip this step

    +

    Create a DNS zone which will contain the managed DNS records. This tutorial will use the fictional domain of example.com.

    +
    aws route53 create-hosted-zone --name "example.com." \
    +  --caller-reference "external-dns-test-$(date +%s)"
    +
    +

    Make a note of the nameservers that were assigned to your new zone.

    +
    ZONE_ID=$(aws route53 list-hosted-zones-by-name --output json \
    +  --dns-name "example.com." --query HostedZones[0].Id --out text)
    +
    +aws route53 list-resource-record-sets --output text \
    + --hosted-zone-id $ZONE_ID --query \
    + "ResourceRecordSets[?Type == 'NS'].ResourceRecords[*].Value | []" | tr '\t' '\n'
    +
    +

    This should yield something similar this:

    +
    ns-695.awsdns-22.net.
    +ns-1313.awsdns-36.org.
    +ns-350.awsdns-43.com.
    +ns-1805.awsdns-33.co.uk.
    +
    +

    If using your own domain that was registered with a third-party domain registrar, you should point your domain’s name servers to the values in the from the list above. Please consult your registrar’s documentation on how to do that.

    +

    Deploy ExternalDNS

    +

    Connect your kubectl client to the cluster you want to test ExternalDNS with.
    +Then apply one of the following manifests file to deploy ExternalDNS. You can check if your cluster has RBAC by kubectl api-versions | grep rbac.authorization.k8s.io.

    +

    For clusters with RBAC enabled, be sure to choose the correct namespace. For this tutorial, the enviornment variable EXTERNALDNS_NS will refer to the namespace. You can set this to a value of your choice:

    +
    export EXTERNALDNS_NS="default" # externaldns, kube-addons, etc
    +
    +# create namespace if it does not yet exist
    +kubectl get namespaces | grep -q $EXTERNALDNS_NS || \
    +  kubectl create namespace $EXTERNALDNS_NS
    +
    +

    Manifest (for clusters without RBAC enabled)

    +

    Save the following below as externaldns-no-rbac.yaml.

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +  labels:
    +    app.kubernetes.io/name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app.kubernetes.io/name: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app.kubernetes.io/name: external-dns
    +    spec:
    +      containers:
    +        - name: external-dns
    +          image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +          args:
    +            - --source=service
    +            - --source=ingress
    +            - --domain-filter=example.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones
    +            - --provider=aws
    +            - --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization
    +            - --aws-zone-type=public # only look at public hosted zones (valid values are public, private or no value for both)
    +            - --registry=txt
    +            - --txt-owner-id=my-hostedzone-identifier
    +          env:
    +            - name: AWS_DEFAULT_REGION
    +              value: us-east-1 # change to region where EKS is installed
    +      # # Uncomment below if using static credentials
    +      #       - name: AWS_SHARED_CREDENTIALS_FILE
    +      #        value: /.aws/credentials
    +      #     volumeMounts:
    +      #       - name: aws-credentials
    +      #         mountPath: /.aws
    +      #         readOnly: true
    +      # volumes:
    +      #   - name: aws-credentials
    +      #     secret:
    +      #       secretName: external-dns
    +
    +

    When ready you can deploy:

    +
    kubectl create --filename externaldns-no-rbac.yaml \
    +  --namespace ${EXTERNALDNS_NS:-"default"}
    +
    +

    Manifest (for clusters with RBAC enabled)

    +

    Save the following below as externaldns-with-rbac.yaml.

    +
    # comment out sa if it was previously created
    +apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +  labels:
    +    app.kubernetes.io/name: external-dns
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRole
    +metadata:
    +  name: external-dns
    +  labels:
    +    app.kubernetes.io/name: external-dns
    +rules:
    +  - apiGroups: [""]
    +    resources: ["services","endpoints","pods","nodes"]
    +    verbs: ["get","watch","list"]
    +  - apiGroups: ["extensions","networking.k8s.io"]
    +    resources: ["ingresses"]
    +    verbs: ["get","watch","list"]
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRoleBinding
    +metadata:
    +  name: external-dns-viewer
    +  labels:
    +    app.kubernetes.io/name: external-dns
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: ClusterRole
    +  name: external-dns
    +subjects:
    +  - kind: ServiceAccount
    +    name: external-dns
    +    namespace: default # change to desired namespace: externaldns, kube-addons
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +  labels:
    +    app.kubernetes.io/name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app.kubernetes.io/name: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app.kubernetes.io/name: external-dns
    +    spec:
    +      serviceAccountName: external-dns
    +      containers:
    +        - name: external-dns
    +          image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +          args:
    +            - --source=service
    +            - --source=ingress
    +            - --domain-filter=example.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones
    +            - --provider=aws
    +            - --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization
    +            - --aws-zone-type=public # only look at public hosted zones (valid values are public, private or no value for both)
    +            - --registry=txt
    +            - --txt-owner-id=external-dns
    +          env:
    +            - name: AWS_DEFAULT_REGION
    +              value: us-east-1 # change to region where EKS is installed
    +     # # Uncommend below if using static credentials
    +     #        - name: AWS_SHARED_CREDENTIALS_FILE
    +     #          value: /.aws/credentials
    +     #      volumeMounts:
    +     #        - name: aws-credentials
    +     #          mountPath: /.aws
    +     #          readOnly: true
    +     #  volumes:
    +     #    - name: aws-credentials
    +     #      secret:
    +     #        secretName: external-dns
    +
    +

    When ready deploy:

    +
    kubectl create --filename externaldns-with-rbac.yaml \
    +  --namespace ${EXTERNALDNS_NS:-"default"}
    +
    +

    Arguments

    +

    This list is not the full list, but a few arguments that where chosen.

    +

    aws-zone-type

    +

    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. To make the target an alias, the ingress needs to be configured correctly as described in the docs. In particular, the argument --publish-service=default/nginx-ingress-controller has to be set on the nginx-ingress-controller container. If one uses the nginx-ingress Helm chart, this flag can be set with the controller.publishService.enabled configuration option.

    +

    target-hosted-zone

    +

    external-dns.alpha.kubernetes.io/aws-target-hosted-zone can optionally be set to the ID of a Route53 hosted zone. This will force external-dns to use the specified hosted zone when creating an ALIAS target.

    +

    Verify ExternalDNS works (Service example)

    +

    Create the following sample application to test that ExternalDNS works.

    +
    +

    For services ExternalDNS will look for the annotation external-dns.alpha.kubernetes.io/hostname on the service and use the corresponding value.

    +

    If you want to give multiple names to service, you can set it to external-dns.alpha.kubernetes.io/hostname with a comma , separator.

    +
    +

    For this verification phase, you can use default or another namespace for the nginx demo, for example:

    +
    NGINXDEMO_NS="nginx"
    +kubectl get namespaces | grep -q $NGINXDEMO_NS || kubectl create namespace $NGINXDEMO_NS
    +
    +

    Save the following manifest below as nginx.yaml:

    +
    apiVersion: v1
    +kind: Service
    +metadata:
    +  name: nginx
    +  annotations:
    +    external-dns.alpha.kubernetes.io/hostname: nginx.example.com
    +spec:
    +  type: LoadBalancer
    +  ports:
    +  - port: 80
    +    name: http
    +    targetPort: 80
    +  selector:
    +    app: nginx
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: nginx
    +spec:
    +  selector:
    +    matchLabels:
    +      app: nginx
    +  template:
    +    metadata:
    +      labels:
    +        app: nginx
    +    spec:
    +      containers:
    +      - image: nginx
    +        name: nginx
    +        ports:
    +        - containerPort: 80
    +          name: http
    +
    +

    Deploy the nginx deployment and service with:

    +
    kubectl create --filename nginx.yaml --namespace ${NGINXDEMO_NS:-"default"}
    +
    +

    Verify that the load balancer was allocated with:

    +
    kubectl get service nginx --namespace ${NGINXDEMO_NS:-"default"}
    +
    +

    This should show something like:

    +
    NAME    TYPE           CLUSTER-IP     EXTERNAL-IP                                                                   PORT(S)        AGE
    +nginx   LoadBalancer   10.100.47.41   ae11c2360188411e7951602725593fd1-1224345803.eu-central-1.elb.amazonaws.com.   80:32749/TCP   12m
    +
    +

    After roughly two minutes check that a corresponding DNS record for your service that was created.

    +
    aws route53 list-resource-record-sets --output json --hosted-zone-id $ZONE_ID \
    +  --query "ResourceRecordSets[?Name == 'nginx.example.com.']|[?Type == 'A']"
    +
    +

    This should show something like:

    +
    [
    +    {
    +        "Name": "nginx.example.com.",
    +        "Type": "A",
    +        "AliasTarget": {
    +            "HostedZoneId": "ZEWFWZ4R16P7IB",
    +            "DNSName": "ae11c2360188411e7951602725593fd1-1224345803.eu-central-1.elb.amazonaws.com.",
    +            "EvaluateTargetHealth": true
    +        }
    +    }
    +]
    +
    +

    You can also fetch the corresponding text records:

    +
    aws route53 list-resource-record-sets --output json --hosted-zone-id $ZONE_ID \
    +  --query "ResourceRecordSets[?Name == 'nginx.example.com.']|[?Type == 'TXT']"
    +
    +

    This will show something like:

    +
    [
    +    {
    +        "Name": "nginx.example.com.",
    +        "Type": "TXT",
    +        "TTL": 300,
    +        "ResourceRecords": [
    +            {
    +                "Value": "\"heritage=external-dns,external-dns/owner=external-dns,external-dns/resource=service/default/nginx\""
    +            }
    +        ]
    +    }
    +]
    +
    +

    Note created TXT record alongside ALIAS record. TXT record signifies that the corresponding ALIAS record is managed by ExternalDNS. This makes ExternalDNS safe for running in environments where there are other records managed via other means.

    +

    For more information about ALIAS record, see Choosing between alias and non-alias records.

    +

    Let’s check that we can resolve this DNS name. We’ll ask the nameservers assigned to your zone first.

    +
    dig +short @ns-5514.awsdns-53.org. nginx.example.com.
    +
    +

    This should return 1+ IP addresses that correspond to the ELB FQDN, i.e. ae11c2360188411e7951602725593fd1-1224345803.eu-central-1.elb.amazonaws.com..

    +

    Next try the public nameservers configured by DNS client on your system:

    +
    dig +short nginx.example.com.
    +
    +

    If you hooked up your DNS zone with its parent zone correctly you can use curl to access your site.

    +
    curl nginx.example.com.
    +
    +

    This should show something like:

    +
    <!DOCTYPE html>
    +<html>
    +<head>
    +<title>Welcome to nginx!</title>
    +...
    +</head>
    +<body>
    +<h1>Welcome to nginx!</h1>
    +...
    +</body>
    +</html>
    +
    +

    Verify ExternalDNS works (Ingress example)

    +

    With the previous deployment and service objects deployed, we can add an ingress object and configure a FQDN value for the host key. The ingress controller will match incoming HTTP traffic, and route it to the appropriate backend service based on the host key.

    +
    +

    For ingress objects ExternalDNS will create a DNS record based on the host specified for the ingress object.

    +
    +

    For this tutorial, we have two endpoints, the service with LoadBalancer type and an ingress. For practical purposes, if an ingress is used, the service type can be changed to ClusterIP as two endpoints are unecessary in this scenario.

    +

    IMPORTANT: This requires that an ingress controller has been installed in your Kubernetes cluster. EKS does not come with an ingress controller by default. A popular ingress controller is ingress-nginx, which can be installed by a helm chart or by manifests.

    +

    Create an ingress resource manifest file named ingress.yaml with the contents below:

    +
    ---
    +apiVersion: networking.k8s.io/v1
    +kind: Ingress
    +metadata:
    +  name: nginx
    +spec:
    +  ingressClassName: nginx
    +  rules:
    +    - host: server.example.com
    +      http:
    +        paths:
    +          - backend:
    +              service:
    +                name: nginx
    +                port:
    +                  number: 80
    +            path: /
    +            pathType: Prefix
    +
    +

    When ready, you can deploy this with:

    +
    kubectl create --filename ingress.yaml --namespace ${NGINXDEMO_NS:-"default"}
    +
    +

    Watch the status of the ingress until the ADDRESS field is populated.

    +
    kubectl get ingress --watch --namespace ${NGINXDEMO_NS:-"default"}
    +
    +

    You should see something like this:

    +
    NAME    CLASS    HOSTS                ADDRESS   PORTS   AGE
    +nginx   <none>   server.example.com             80      47s
    +nginx   <none>   server.example.com   ae11c2360188411e7951602725593fd1-1224345803.eu-central-1.elb.amazonaws.com.   80      54s
    +
    +

    For the ingress test, run through similar checks, but using domain name used for the ingress:

    +
    # check records on route53
    +aws route53 list-resource-record-sets --output json --hosted-zone-id $ZONE_ID \
    +  --query "ResourceRecordSets[?Name == 'server.example.com.']"
    +
    +# query using a route53 name server
    +dig +short @ns-5514.awsdns-53.org. server.example.com.
    +# query using the default name server
    +dig +short server.example.com.
    +
    +# connect to the nginx web server through the ingress
    +curl server.example.com.
    +
    +

    More service annotation options

    +

    Custom TTL

    +

    The default DNS record TTL (Time-To-Live) is 300 seconds. You can customize this value by setting the annotation external-dns.alpha.kubernetes.io/ttl.
    +e.g., modify the service manifest YAML file above:

    +
    apiVersion: v1
    +kind: Service
    +metadata:
    +  name: nginx
    +  annotations:
    +    external-dns.alpha.kubernetes.io/hostname: nginx.example.com
    +    external-dns.alpha.kubernetes.io/ttl: "60"
    +spec:
    +    ...
    +
    +

    This will set the DNS record’s TTL to 60 seconds.

    +

    Routing policies

    +

    Route53 offers different routing policies. The routing policy for a record can be controlled with the following annotations:

    +
      +
    • external-dns.alpha.kubernetes.io/set-identifier: this needs to be set to use any of the following routing policies
    • +
    +

    For any given DNS name, only one of the following routing policies can be used:

    +
      +
    • Weighted records: external-dns.alpha.kubernetes.io/aws-weight
    • +
    • Latency-based routing: external-dns.alpha.kubernetes.io/aws-region
    • +
    • Failover:external-dns.alpha.kubernetes.io/aws-failover
    • +
    • Geolocation-based routing:
    • +
    • external-dns.alpha.kubernetes.io/aws-geolocation-continent-code
    • +
    • external-dns.alpha.kubernetes.io/aws-geolocation-country-code
    • +
    • external-dns.alpha.kubernetes.io/aws-geolocation-subdivision-code
    • +
    • Multi-value answer:external-dns.alpha.kubernetes.io/aws-multi-value-answer
    • +
    +

    Associating DNS records with healthchecks

    +

    You can configure Route53 to associate DNS records with healthchecks for automated DNS failover using
    +external-dns.alpha.kubernetes.io/aws-health-check-id: <health-check-id> annotation.

    +

    Note: ExternalDNS does not support creating healthchecks, and assumes that <health-check-id> already exists.

    +

    Canonical Hosted Zones

    +

    When creating ALIAS type records in Route53 it is required that external-dns be aware of the canonical hosted zone in which
    +the specified hostname is created. External-dns is able to automatically identify the canonical hosted zone for many
    +hostnames based upon known hostname suffixes which are defined in aws.go. If a hostname
    +does not have a known suffix then the suffix can be added into aws.go or the target-hosted-zone annotation
    +can be used to manually define the ID of the canonical hosted zone.

    +

    Govcloud caveats

    +

    Due to the special nature with how Route53 runs in Govcloud, there are a few tweaks in the deployment settings.

    +
      +
    • An Environment variable with name of AWS_REGION set to either us-gov-west-1 or us-gov-east-1 is required. Otherwise it tries to lookup a region that does not exist in Govcloud and it errors out.
    • +
    +
    env:
    +- name: AWS_REGION
    +  value: us-gov-west-1
    +
    +
      +
    • Route53 in Govcloud does not allow aliases. Therefore, container args must be set so that it uses CNAMES and a txt-prefix must be set to something. Otherwise, it will try to create a TXT record with the same value than the CNAME itself, which is not allowed.
    • +
    +
    args:
    +- --aws-prefer-cname
    +- --txt-prefix={{ YOUR_PREFIX }}
    +
    +
      +
    • The first two changes are needed if you use Route53 in Govcloud, which only supports private zones. There are also no cross account IAM whatsoever between Govcloud and commercial AWS accounts. If services and ingresses need to make Route 53 entries to an public zone in a commercial account, you will have set env variables of AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY with a key and secret to the commercial account that has the sufficient rights.
    • +
    +
    env:
    +- name: AWS_ACCESS_KEY_ID
    +  value: XXXXXXXXX
    +- name: AWS_SECRET_ACCESS_KEY
    +  valueFrom:
    +    secretKeyRef:
    +      name: {{ YOUR_SECRET_NAME }}
    +      key: {{ YOUR_SECRET_KEY }}
    +
    +

    Clean up

    +

    Make sure to delete all Service objects before terminating the cluster so all load balancers get cleaned up correctly.

    +
    kubectl delete service nginx
    +
    +

    IMPORTANT If you attached a policy to the Node IAM Role, then you will want to detach this before deleting the EKS cluster. Otherwise, the role resource will be locked, and the cluster cannot be deleted, especially if it was provisioned by automation like terraform or eksctl.

    +
    aws iam detach-role-policy --role-name $NODE_ROLE_NAME --policy-arn $POLICY_ARN
    +
    +

    If the cluster was provisioned using eksctl, you can delete the cluster with:

    +
    eksctl delete cluster --name $EKS_CLUSTER_NAME --region $EKS_CLUSTER_REGION
    +
    +

    Give ExternalDNS some time to clean up the DNS records for you. Then delete the hosted zone if you created one for the testing purpose.

    +
    aws route53 delete-hosted-zone --id $NODE_ID # e.g /hostedzone/ZEWFWZ4R16P7IB
    +
    +

    If IAM user credentials were used, you can remove the user with:

    +
    aws iam detach-user-policy --user-name "externaldns" --policy-arn $POLICY_ARN
    +aws iam delete-user --user-name "externaldns"
    +
    +

    If IRSA was used, you can remove the IRSA role with:

    +
    aws iam detach-role-policy --role-name $IRSA_ROLE --policy-arn $POLICY_ARN
    +aws iam delete-role --role-name $IRSA_ROLE
    +
    +

    Delete any unneeded policies:

    +
    aws iam delete-policy --policy-arn $POLICY_ARN
    +
    +

    Throttling

    +

    Route53 has a 5 API requests per second per account hard quota.
    +Running several fast polling ExternalDNS instances in a given account can easily hit that limit. Some ways to reduce the request rate include:
    +* Reduce the polling loop’s synchronization interval at the possible cost of slower change propagation (but see --events below to reduce the impact).
    + * --interval=5m (default 1m)
    +* Trigger the polling loop on changes to K8s objects, rather than only at interval, to have responsive updates with long poll intervals
    + * --events
    +* Limit the sources watched when the --events flag is specified to specific types, namespaces, labels, or annotations
    + * --source=ingress --source=service - specify multiple times for multiple sources
    + * --namespace=my-app
    + * --label-filter=app in (my-app)
    + * --ingress-class=nginx-external
    +* Limit services watched by type (not applicable to ingress or other types)
    + * --service-type-filter=LoadBalancer default all
    +* Limit the hosted zones considered
    + * --zone-id-filter=ABCDEF12345678 - specify multiple times if needed
    + * --domain-filter=example.com by domain suffix - specify multiple times if needed
    + * --regex-domain-filter=example* by domain suffix but as a regex - overrides domain-filter
    + * --exclude-domains=ignore.this.example.com to exclude a domain or subdomain
    + * --regex-domain-exclusion=ignore* subtracts it’s matches from regex-domain-filter’s matches
    + * --aws-zone-type=public only sync zones of this type [public|private]
    + * --aws-zone-tags=owner=k8s only sync zones with this tag
    +* If the list of zones managed by ExternalDNS doesn’t change frequently, cache it by setting a TTL.
    + * --aws-zones-cache-duration=3h (default 0 - disabled)
    +* Increase the number of changes applied to Route53 in each batch
    + * --aws-batch-change-size=4000 (default 1000)
    +* Increase the interval between changes
    + * --aws-batch-change-interval=10s (default 1s)
    +* Introducing some jitter to the pod initialization, so that when multiple instances of ExternalDNS are updated at the same time they do not make their requests on the same second.

    +

    A simple way to implement randomised startup is with an init container:

    +
    ...
    +    spec:
    +      initContainers:
    +      - name: init-jitter
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        command:
    +        - /bin/sh
    +        - -c
    +        - 'FOR=$((RANDOM % 10))s;echo "Sleeping for $FOR";sleep $FOR'
    +      containers:
    +...
    +
    +

    EKS

    +

    An effective starting point for EKS with an ingress controller might look like:

    +
    --interval=5m
    +--events
    +--source=ingress
    +--domain-filter=example.com
    +--aws-zones-cache-duration=1h
    +
    + +
    +
    + + + Last update: + June 23, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/azure-private-dns/index.html b/v0.13.6/tutorials/azure-private-dns/index.html new file mode 100644 index 0000000000..12bef7c0c0 --- /dev/null +++ b/v0.13.6/tutorials/azure-private-dns/index.html @@ -0,0 +1,2517 @@ + + + + + + + + + + + + + + + + + + Set up ExternalDNS for Azure Private DNS - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + + + + +
    +
    + + + + + + + + +

    Set up ExternalDNS for Azure Private DNS

    +

    This tutorial describes how to set up ExternalDNS for managing records in Azure Private DNS.

    +

    It comprises of the following steps:
    +1) Provision Azure Private DNS
    +2) Configure service principal for managing the zone
    +3) Deploy ExternalDNS
    +4) Expose an NGINX service with a LoadBalancer and annotate it with the desired DNS name
    +5) Install NGINX Ingress Controller (Optional)
    +6) Expose an nginx service with an ingress (Optional)
    +7) Verify the DNS records

    +

    Everything will be deployed on Kubernetes.
    +Therefore, please see the subsequent prerequisites.

    +

    Prerequisites

    +
      +
    • Azure Kubernetes Service is deployed and ready
    • +
    • Azure CLI 2.0 and kubectl installed on the box to execute the subsequent steps
    • +
    +

    Provision Azure Private DNS

    +

    The provider will find suitable zones for domains it manages. It will
    +not automatically create zones.

    +

    For this tutorial, we will create a Azure resource group named ‘externaldns’ that can easily be deleted later.

    +
    $ az group create -n externaldns -l westeurope
    +
    +

    Substitute a more suitable location for the resource group if desired.

    +

    As a prerequisite for Azure Private DNS to resolve records is to define links with VNETs.
    +Thus, first create a VNET.

    +
    $ az network vnet create \
    +  --name myvnet \
    +  --resource-group externaldns \
    +  --location westeurope \
    +  --address-prefix 10.2.0.0/16 \
    +  --subnet-name mysubnet \
    +  --subnet-prefixes 10.2.0.0/24
    +
    +

    Next, create a Azure Private DNS zone for “example.com”:

    +
    $ az network private-dns zone create -g externaldns -n example.com
    +
    +

    Substitute a domain you own for “example.com” if desired.

    +

    Finally, create the mentioned link with the VNET.

    +
    $ az network private-dns link vnet create -g externaldns -n mylink \
    +   -z example.com -v myvnet --registration-enabled false
    +
    +

    Configure service principal for managing the zone

    +

    ExternalDNS needs permissions to make changes in Azure Private DNS.
    +These permissions are roles assigned to the service principal used by ExternalDNS.

    +

    A service principal with a minimum access level of Private DNS Zone Contributor to the Private DNS zone(s) and Reader to the resource group containing the Azure Private DNS zone(s) is necessary.
    +More powerful role-assignments like Owner or assignments on subscription-level work too.

    +

    Start off by creating the service principal without role-assignments.
    +

    $ az ad sp create-for-rbac --skip-assignment -n http://externaldns-sp
    +{
    +  "appId": "appId GUID",  <-- aadClientId value
    +  ...
    +  "password": "password",  <-- aadClientSecret value
    +  "tenant": "AzureAD Tenant Id"  <-- tenantId value
    +}
    +

    +
    +

    Note: Alternatively, you can issue az account show --query "tenantId" to retrieve the id of your AAD Tenant too.

    +
    +

    Next, assign the roles to the service principal.
    +But first retrieve the ID’s of the objects to assign roles on.

    +

    # find out the resource ids of the resource group where the dns zone is deployed, and the dns zone itself
    +$ az group show --name externaldns --query id -o tsv
    +/subscriptions/id/resourceGroups/externaldns
    +
    +$ az network private-dns zone show --name example.com -g externaldns --query id -o tsv
    +/subscriptions/.../resourceGroups/externaldns/providers/Microsoft.Network/privateDnsZones/example.com
    +

    +Now, create role assignments.
    +
    # 1. as a reader to the resource group
    +$ az role assignment create --role "Reader" --assignee <appId GUID> --scope <resource group resource id>
    +
    +# 2. as a contributor to DNS Zone itself
    +$ az role assignment create --role "Private DNS Zone Contributor" --assignee <appId GUID> --scope <dns zone resource id>
    +

    +

    Deploy ExternalDNS

    +

    Configure kubectl to be able to communicate and authenticate with your cluster.
    +This is per default done through the file ~/.kube/config.

    +

    For general background information on this see kubernetes-docs.
    +Azure-CLI features functionality for automatically maintaining this file for AKS-Clusters. See Azure-Docs.

    +

    Follow the steps for azure-dns provider to create a configuration file.

    +

    Then apply one of the following manifests depending on whether you use RBAC or not.

    +

    The credentials of the service principal are provided to ExternalDNS as environment-variables.

    +

    Manifest (for clusters without RBAC enabled)

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: externaldns
    +spec:
    +  selector:
    +    matchLabels:
    +      app: externaldns
    +  strategy:
    +    type: Recreate
    +  template:
    +    metadata:
    +      labels:
    +        app: externaldns
    +    spec:
    +      containers:
    +      - name: externaldns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service
    +        - --source=ingress
    +        - --domain-filter=example.com
    +        - --provider=azure-private-dns
    +        - --azure-resource-group=externaldns
    +        - --azure-subscription-id=<use the id of your subscription>
    +        volumeMounts:
    +        - name: azure-config-file
    +          mountPath: /etc/kubernetes
    +          readOnly: true
    +      volumes:
    +      - name: azure-config-file
    +        secret:
    +          secretName: azure-config-file
    +
    +

    Manifest (for clusters with RBAC enabled, cluster access)

    +
    apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: externaldns
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRole
    +metadata:
    +  name: externaldns
    +rules:
    +- apiGroups: [""]
    +  resources: ["services","endpoints","pods"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: ["extensions","networking.k8s.io"]
    +  resources: ["ingresses"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: [""]
    +  resources: ["nodes"]
    +  verbs: ["get", "watch", "list"]
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRoleBinding
    +metadata:
    +  name: externaldns-viewer
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: ClusterRole
    +  name: externaldns
    +subjects:
    +- kind: ServiceAccount
    +  name: externaldns
    +  namespace: default
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: externaldns
    +spec:
    +  selector:
    +    matchLabels:
    +      app: externaldns
    +  strategy:
    +    type: Recreate
    +  template:
    +    metadata:
    +      labels:
    +        app: externaldns
    +    spec:
    +      serviceAccountName: externaldns
    +      containers:
    +      - name: externaldns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service
    +        - --source=ingress
    +        - --domain-filter=example.com
    +        - --provider=azure-private-dns
    +        - --azure-resource-group=externaldns
    +        - --azure-subscription-id=<use the id of your subscription>
    +        volumeMounts:
    +        - name: azure-config-file
    +          mountPath: /etc/kubernetes
    +          readOnly: true
    +      volumes:
    +      - name: azure-config-file
    +        secret:
    +          secretName: azure-config-file
    +
    +

    Manifest (for clusters with RBAC enabled, namespace access)

    +

    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 requires cluster access, so when using this manifest,
    +services with type NodePort will be skipped!

    +
    apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: externaldns
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: Role
    +metadata:
    +  name: externaldns
    +rules:
    +- apiGroups: [""]
    +  resources: ["services","endpoints","pods"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: ["extensions","networking.k8s.io"]
    +  resources: ["ingresses"]
    +  verbs: ["get","watch","list"]
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: RoleBinding
    +metadata:
    +  name: externaldns
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: Role
    +  name: externaldns
    +subjects:
    +- kind: ServiceAccount
    +  name: externaldns
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: externaldns
    +spec:
    +  selector:
    +    matchLabels:
    +      app: externaldns
    +  strategy:
    +    type: Recreate
    +  template:
    +    metadata:
    +      labels:
    +        app: externaldns
    +    spec:
    +      serviceAccountName: externaldns
    +      containers:
    +      - name: externaldns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service
    +        - --source=ingress
    +        - --domain-filter=example.com
    +        - --provider=azure-private-dns
    +        - --azure-resource-group=externaldns
    +        - --azure-subscription-id=<use the id of your subscription>
    +        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:

    +
    $ kubectl create -f externaldns.yaml
    +
    +

    Create an nginx deployment

    +

    This step creates a demo workload in your cluster. Apply the following manifest to create a deployment that we are going to expose later in this tutorial in multiple ways:

    +
    ---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: nginx
    +spec:
    +  selector:
    +    matchLabels:
    +      app: nginx
    +  template:
    +    metadata:
    +      labels:
    +        app: nginx
    +    spec:
    +      containers:
    +        - image: nginx
    +          name: nginx
    +          ports:
    +          - containerPort: 80
    +
    +

    Expose the nginx deployment with a load balancer

    +

    Apply the following manifest to create a service of type LoadBalancer. This will create a public load balancer in Azure that will forward traffic to the nginx pods.

    +
    ---
    +apiVersion: v1
    +kind: Service
    +annotations:
    +  service.beta.kubernetes.io/azure-load-balancer-internal: "true"
    +  external-dns.alpha.kubernetes.io/hostname: server.example.com
    +  external-dns.alpha.kubernetes.io/internal-hostname: server-clusterip.example.com
    +metadata:
    +  name: nginx-svc
    +spec:
    +  ports:
    +    - port: 80
    +      protocol: TCP
    +      targetPort: 80
    +  selector:
    +    app: nginx
    +  type: LoadBalancer
    +
    +

    In the service we used multiple annptations. The annotation service.beta.kubernetes.io/azure-load-balancer-internal is used to create an internal load balancer. The annotation external-dns.alpha.kubernetes.io/hostname is used to create a DNS record for the load balancer that will point to the internal IP address in the VNET allocated by the internal load balancer. The annotation external-dns.alpha.kubernetes.io/internal-hostname is used to create a private DNS record for the load balancer that will point to the cluster IP.

    +

    Install NGINX Ingress Controller (Optional)

    +

    Helm is used to deploy the ingress controller.

    +

    We employ the popular chart ingress-nginx.

    +
    $ helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
    +$ helm repo update
    +$ helm install [RELEASE_NAME] ingress-nginx/ingress-nginx
    +     --set controller.publishService.enabled=true
    +
    +

    The parameter controller.publishService.enabled needs to be set to true.

    +

    It will make the ingress controller update the endpoint records of ingress-resources to contain the external-ip of the loadbalancer serving the ingress-controller.
    +This is crucial as ExternalDNS reads those endpoints records when creating DNS-Records from ingress-resources.
    +In the subsequent parameter we will make use of this. If you don’t want to work with ingress-resources in your later use, you can leave the parameter out.

    +

    Verify the correct propagation of the loadbalancer’s ip by listing the ingresses.

    +
    $ kubectl get ingress
    +
    +

    The address column should contain the ip for each ingress. ExternalDNS will pick up exactly this piece of information.

    +
    NAME     HOSTS             ADDRESS          PORTS   AGE
    +nginx1   sample1.aks.com   52.167.195.110   80      6d22h
    +nginx2   sample2.aks.com   52.167.195.110   80      6d21h
    +
    +

    If you do not want to deploy the ingress controller with Helm, ensure to pass the following cmdline-flags to it through the mechanism of your choice:

    +
    flags:
    +--publish-service=<namespace of ingress-controller >/<svcname of ingress-controller>
    +--update-status=true (default-value)
    +
    +example:
    +./nginx-ingress-controller --publish-service=default/nginx-ingress-controller
    +
    +

    Expose the nginx deployment with the ingress (Optional)

    +

    Apply the following manifest to create an ingress resource that will expose the nginx deployment. The ingress resource backend points to a ClusterIP service that is needed to select the pods that will receive the traffic.

    +
    ---
    +apiVersion: v1
    +kind: Service
    +metadata:
    +  name: nginx-svc-clusterip
    +spec:
    +  ports:
    +  - port: 80
    +    protocol: TCP
    +    targetPort: 80
    +  selector:
    +    app: nginx
    +  type: ClusterIP
    +
    +---
    +apiVersion: networking.k8s.io/v1
    +kind: Ingress
    +metadata:
    +  name: nginx
    +spec:
    +  ingressClassName: nginx
    +  rules:
    +  - host: server.example.com
    +    http:
    +      paths:
    +      - backend:
    +          service:
    +            name: nginx-svc-clusterip
    +            port:
    +              number: 80
    +        pathType: Prefix
    +
    +

    When using ExternalDNS with ingress objects it will automatically create DNS records based on host names specified in ingress objects that match the domain-filter argument in the externaldns deployment manifest. When those host names are removed or renamed the corresponding DNS records are also altered.

    +

    Create the deployment, service and ingress object:

    +
    $ kubectl create -f nginx.yaml
    +
    +

    Since your external IP would have already been assigned to the nginx-ingress service, the DNS records pointing to the IP of the nginx-ingress service should be created within a minute.

    +

    Verify created records

    +

    Run the following command to view the A records for your Azure Private DNS zone:

    +
    $ az network private-dns record-set a list -g externaldns -z example.com
    +
    +

    Substitute the zone for the one created above if a different domain was used.

    +

    This should show the external IP address of the service as the A record for your domain (‘@’ indicates the record is for the zone itself).

    + +
    +
    + + + Last update: + May 29, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/azure/index.html b/v0.13.6/tutorials/azure/index.html new file mode 100644 index 0000000000..422e4c072e --- /dev/null +++ b/v0.13.6/tutorials/azure/index.html @@ -0,0 +1,3183 @@ + + + + + + + + + + + + + + + + + + Setting up ExternalDNS for Services on Azure - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + +
    +
    +
    + + + +
    +
    +
    + + +
    +
    + + + + + + + + +

    Setting up ExternalDNS for Services on Azure

    +

    This tutorial describes how to setup ExternalDNS for Azure DNS with Azure Kubernetes Service.

    +

    Make sure to use >=0.11.0 version of ExternalDNS for this tutorial.

    +

    This tutorial uses Azure CLI 2.0 for all
    +Azure commands and assumes that the Kubernetes cluster was created via Azure Container Services and kubectl commands
    +are being run on an orchestration node.

    +

    Creating an Azure DNS zone

    +

    The Azure provider for ExternalDNS will find suitable zones for domains it manages; it will not automatically create zones.

    +

    For this tutorial, we will create a Azure resource group named MyDnsResourceGroup that can easily be deleted later:

    +
    $ az group create --name "MyDnsResourceGroup" --location "eastus"
    +
    +

    Substitute a more suitable location for the resource group if desired.

    +

    Next, create a Azure DNS zone for example.com:

    +
    $ az network dns zone create --resource-group "MyDnsResourceGroup" --name "example.com"
    +
    +

    Substitute a domain you own for example.com if desired.

    +

    If using your own domain that was registered with a third-party domain registrar, you should point your domain’s name servers to the values in the nameServers field from the JSON data returned by the az network dns zone create command. Please consult your registrar’s documentation on how to do that.

    +

    Configuration file

    +

    The azure provider will reference a configuration file called azure.json. The preferred way to inject the configuration file is by using a Kubernetes secret. The secret should contain an object named azure.json with content similar to this:

    +
    {
    +  "tenantId": "01234abc-de56-ff78-abc1-234567890def",
    +  "subscriptionId": "01234abc-de56-ff78-abc1-234567890def",
    +  "resourceGroup": "MyDnsResourceGroup",
    +  "aadClientId": "01234abc-de56-ff78-abc1-234567890def",
    +  "aadClientSecret": "uKiuXeiwui4jo9quae9o"
    +}
    +
    +

    The following fields are used:

    +
      +
    • tenantId (required) - run az account show --query "tenantId" or by selecting Azure Active Directory in the Azure Portal and checking the Directory ID under Properties.
    • +
    • subscriptionId (required) - run az account show --query "id" or by selecting Subscriptions in the Azure Portal.
    • +
    • resourceGroup (required) is the Resource Group created in a previous step that contains the Azure DNS Zone.
    • +
    • aadClientID and aadClientSecret are associated with the Service Principal. This is only used with Service Principal method documented in the next section.
    • +
    • useManagedIdentityExtension - this is set to true if you use either AKS Kubelet Identity or AAD Pod Identities methods documented in the next section.
    • +
    • userAssignedIdentityID - this contains the client id from the Managed identitty when using the AAD Pod Identities method documented in the next setion.
    • +
    • useWorkloadIdentityExtension - this is set to true if you use Workload Identity method documented in the next section.
    • +
    +

    The Azure DNS provider expects, by default, that the configuration file is at /etc/kubernetes/azure.json. This can be overridden with the --azure-config-file option when starting ExternalDNS.

    +

    Permissions to modify DNS zone

    +

    ExternalDNS needs permissions to make changes to the Azure DNS zone. There are three ways configure the access needed:

    + +

    Service Principal

    +

    These permissions are defined in a Service Principal that should be made available to ExternalDNS as a configuration file azure.json.

    +

    Creating a service principal

    +

    A Service Principal with a minimum access level of DNS Zone Contributor or Contributor to the DNS zone(s) and Reader to the resource group containing the Azure DNS zone(s) is necessary for ExternalDNS to be able to edit DNS records. However, other more permissive access levels will work too (e.g. Contributor to the resource group or the whole subscription).

    +

    This is an Azure CLI example on how to query the Azure API for the information required for the Resource Group and DNS zone you would have already created in previous steps (requires azure-cli and jq)

    +
    $ EXTERNALDNS_NEW_SP_NAME="ExternalDnsServicePrincipal" # name of the service principal
    +$ AZURE_DNS_ZONE_RESOURCE_GROUP="MyDnsResourceGroup" # name of resource group where dns zone is hosted
    +$ AZURE_DNS_ZONE="example.com" # DNS zone name like example.com or sub.example.com
    +
    +# Create the service principal
    +$ DNS_SP=$(az ad sp create-for-rbac --name $EXTERNALDNS_NEW_SP_NAME)
    +$ EXTERNALDNS_SP_APP_ID=$(echo $DNS_SP | jq -r '.appId')
    +$ EXTERNALDNS_SP_PASSWORD=$(echo $DNS_SP | jq -r '.password')
    +
    +

    Assign the rights for the service principal

    +

    Grant access to Azure DNS zone for the service principal.

    +
    # fetch DNS id used to grant access to the service principal
    +DNS_ID=$(az network dns zone show --name $AZURE_DNS_ZONE \
    + --resource-group $AZURE_DNS_ZONE_RESOURCE_GROUP --query "id" --output tsv)
    +
    +# 1. as a reader to the resource group
    +$ az role assignment create --role "Reader" --assignee $EXTERNALDNS_SP_APP_ID --scope $DNS_ID
    +
    +# 2. as a contributor to DNS Zone itself
    +$ az role assignment create --role "Contributor" --assignee $EXTERNALDNS_SP_APP_ID --scope $DNS_ID
    +
    +

    Creating a configuration file for the service principal

    +

    Create the file azure.json with values gather from previous steps.

    +
    cat <<-EOF > /local/path/to/azure.json
    +{
    +  "tenantId": "$(az account show --query tenantId -o tsv)",
    +  "subscriptionId": "$(az account show --query id -o tsv)",
    +  "resourceGroup": "$AZURE_DNS_ZONE_RESOURCE_GROUP",
    +  "aadClientId": "$EXTERNALDNS_SP_APP_ID",
    +  "aadClientSecret": "$EXTERNALDNS_SP_PASSWORD"
    +}
    +EOF
    +
    +

    Use this file to create a Kubernetes secret:

    +
    $ kubectl create secret generic azure-config-file --namespace "default" --from-file /local/path/to/azure.json
    +
    +

    Managed identity using AKS Kubelet identity

    +

    The managed identity that is assigned to the underlying node pool in the AKS cluster can be given permissions to access Azure DNS. Managed identities are essentially a service principal whose lifecycle is managed, such as deleting the AKS cluster will also delete the service principals associated with the AKS cluster. The managed identity assigned Kuberetes node pool, or specifically the VMSS, is called the Kubelet identity.

    +

    The managed identites were previously called MSI (Managed Service Identity) and are enabled by default when creating an AKS cluster.

    +

    Note that permissions granted to this identity will be accessible to all containers running inside the Kubernetes cluster, not just the ExternalDNS container(s).

    +

    For the managed identity, the contents of azure.json should be similar to this:

    +
    {
    +  "tenantId": "01234abc-de56-ff78-abc1-234567890def",
    +  "subscriptionId": "01234abc-de56-ff78-abc1-234567890def",
    +  "resourceGroup": "MyDnsResourceGroup",
    +  "useManagedIdentityExtension": true
    +}
    +
    +

    Fetching the Kubelet identity

    +

    For this process, you will need to get the kublet identity:

    +
    $ PRINCIPAL_ID=$(az aks show --resource-group $CLUSTER_GROUP --name $CLUSTERNAME \
    +  --query "identityProfile.kubeletidentity.objectId" --output tsv)
    +
    +

    Assign rights for the Kubelet identity

    +

    Grant access to Azure DNS zone for the kublet identity.

    +
    $ AZURE_DNS_ZONE="example.com" # DNS zone name like example.com or sub.example.com
    +$ AZURE_DNS_ZONE_RESOURCE_GROUP="MyDnsResourceGroup" # resource group where DNS zone is hosted
    +
    +# fetch DNS id used to grant access to the kublet identity
    +$ DNS_ID=$(az network dns zone show --name $AZURE_DNS_ZONE \
    +  --resource-group $AZURE_DNS_ZONE_RESOURCE_GROUP --query "id" --output tsv)
    +
    +$ az role assignment create --role "DNS Zone Contributor" --assignee $PRINCIPAL_ID --scope $DNS_ID
    +
    +

    Creating a configuration file for the kubelet identity

    +

    Create the file azure.json with values gather from previous steps.

    +
    cat <<-EOF > /local/path/to/azure.json
    +{
    +  "tenantId": "$(az account show --query tenantId -o tsv)",
    +  "subscriptionId": "$(az account show --query id -o tsv)",
    +  "resourceGroup": "$AZURE_DNS_ZONE_RESOURCE_GROUP",
    +  "useManagedIdentityExtension": true
    +}
    +EOF
    +
    +

    Use the azure.json file to create a Kubernetes secret:

    +
    $ kubectl create secret generic azure-config-file --namespace "default" --from-file /local/path/to/azure.json
    +
    +

    Managed identity using AAD Pod Identities

    +

    For this process, we will create a managed identity that will be explicitly used by the ExternalDNS container. This process is similar to Kubelet identity except that this managed identity is not associated with the Kubernetes node pool, but rather associated with explicit ExternalDNS containers.

    +

    Enable the AAD Pod Identities feature

    +

    For this solution, AAD Pod Identities preview feature can be enabled. The commands below should do the trick to enable this feature:

    +
    $ az feature register --name EnablePodIdentityPreview --namespace Microsoft.ContainerService
    +$ az feature register --name AutoUpgradePreview --namespace Microsoft.ContainerService
    +$ az extension add --name aks-preview
    +$ az extension update --name aks-preview
    +$ az provider register --namespace Microsoft.ContainerService
    +
    +

    Deploy the AAD Pod Identities service

    +

    Once enabled, you can update your cluster and install needed services for the AAD Pod Identities feature.

    +
    $ AZURE_AKS_RESOURCE_GROUP="my-aks-cluster-group" # name of resource group where aks cluster was created
    +$ AZURE_AKS_CLUSTER_NAME="my-aks-cluster" # name of aks cluster previously created
    +
    +$ az aks update --resource-group ${AZURE_AKS_RESOURCE_GROUP} --name ${AZURE_AKS_CLUSTER_NAME} --enable-pod-identity
    +
    +

    Note that, if you use the default network plugin kubenet, then you need to add the command line option --enable-pod-identity-with-kubenet to the above command.

    +

    Creating the managed identity

    +

    After this process is finished, create a managed identity.

    +
    $ IDENTITY_RESOURCE_GROUP=$AZURE_AKS_RESOURCE_GROUP # custom group or reuse AKS group
    +$ IDENTITY_NAME="example-com-identity"
    +
    +# create a managed identity
    +$ az identity create --resource-group "${IDENTITY_RESOURCE_GROUP}" --name "${IDENTITY_NAME}"
    +
    +

    Assign rights for the managed identity

    +

    Grant access to Azure DNS zone for the managed identity.

    +
    $ AZURE_DNS_ZONE_RESOURCE_GROUP="MyDnsResourceGroup" # name of resource group where dns zone is hosted
    +$ AZURE_DNS_ZONE="example.com" # DNS zone name like example.com or sub.example.com
    +
    +# fetch identity client id from managed identity created earlier
    +$ IDENTITY_CLIENT_ID=$(az identity show --resource-group "${IDENTITY_RESOURCE_GROUP}" \
    +  --name "${IDENTITY_NAME}" --query "clientId" --output tsv)
    +# fetch DNS id used to grant access to the managed identity
    +$ DNS_ID=$(az network dns zone show --name "${AZURE_DNS_ZONE}" \
    +  --resource-group "${AZURE_DNS_ZONE_RESOURCE_GROUP}" --query "id" --output tsv)
    +
    +$ az role assignment create --role "DNS Zone Contributor" \
    +  --assignee "${IDENTITY_CLIENT_ID}" --scope "${DNS_ID}"
    +
    +

    Creating a configuration file for the managed identity

    +

    Create the file azure.json with the values from previous steps:

    +
    cat <<-EOF > /local/path/to/azure.json
    +{
    +  "tenantId": "$(az account show --query tenantId -o tsv)",
    +  "subscriptionId": "$(az account show --query id -o tsv)",
    +  "resourceGroup": "$AZURE_DNS_ZONE_RESOURCE_GROUP",
    +  "useManagedIdentityExtension": true,
    +  "userAssignedIdentityID": "$IDENTITY_CLIENT_ID"
    +}
    +EOF
    +
    +

    Use the azure.json file to create a Kubernetes secret:

    +
    $ kubectl create secret generic azure-config-file --namespace "default" --from-file /local/path/to/azure.json
    +
    +

    Creating an Azure identity binding

    +

    A binding between the managed identity and the ExternalDNS pods needs to be setup by creating AzureIdentity and AzureIdentityBinding resources. This will allow appropriately labeled ExternalDNS pods to authenticate using the managed identity. When AAD Pod Identity feature is enabled from previous steps above, the az aks pod-identity add can be used to create these resources:

    +
    $ IDENTITY_RESOURCE_ID=$(az identity show --resource-group ${IDENTITY_RESOURCE_GROUP} \
    +  --name ${IDENTITY_NAME} --query id --output tsv)
    +
    +$ az aks pod-identity add --resource-group ${AZURE_AKS_RESOURCE_GROUP}  \
    +  --cluster-name ${AZURE_AKS_CLUSTER_NAME} --namespace "default" \
    +  --name "external-dns" --identity-resource-id ${IDENTITY_RESOURCE_ID}
    +
    +

    This will add something similar to the following resouces:

    +
    apiVersion: aadpodidentity.k8s.io/v1
    +kind: AzureIdentity
    +metadata:
    +  labels:
    +    addonmanager.kubernetes.io/mode: Reconcile
    +    kubernetes.azure.com/managedby: aks
    +  name: external-dns
    +spec:
    +  clientID: $IDENTITY_CLIENT_ID
    +  resourceID: $IDENTITY_RESOURCE_ID
    +  type: 0
    +---
    +apiVersion: aadpodidentity.k8s.io/v1
    +kind: AzureIdentityBinding
    +metadata:
    +  annotations:
    +  labels:
    +    addonmanager.kubernetes.io/mode: Reconcile
    +    kubernetes.azure.com/managedby: aks
    +  name: external-dns-binding
    +spec:
    +  azureIdentity: external-dns
    +  selector: external-dns
    +
    +

    Update ExternalDNS labels

    +

    When deploying ExternalDNS, you want to make sure that deployed pod(s) will have the label aadpodidbinding: external-dns to enable AAD Pod Identities. You can patch an existing deployment of ExternalDNS with this command:

    +
    kubectl patch deployment external-dns --namespace "default" --patch \
    + '{"spec": {"template": {"metadata": {"labels": {"aadpodidbinding": "external-dns"}}}}}'
    +
    +

    Managed identity using Workload Identity

    +

    For this process, we will create a managed identity that will be explicitly used by the ExternalDNS container. This process is somewhat similar to Pod Identity except that this managed identity is associated with a kubernetes service account.

    +

    Deploy OIDC issuer and Workload Identity services

    +

    Update your cluster to install OIDC Issuer and Workload Identity:

    +
    $ AZURE_AKS_RESOURCE_GROUP="my-aks-cluster-group" # name of resource group where aks cluster was created
    +$ AZURE_AKS_CLUSTER_NAME="my-aks-cluster" # name of aks cluster previously created
    +
    +$ az aks update --resource-group ${AZURE_AKS_RESOURCE_GROUP} --name ${AZURE_AKS_CLUSTER_NAME} --enable-oidc-issuer --enable-workload-identity
    +
    +

    Create a managed identity

    +

    Create a managed identity:

    +
    $ IDENTITY_RESOURCE_GROUP=$AZURE_AKS_RESOURCE_GROUP # custom group or reuse AKS group
    +$ IDENTITY_NAME="example-com-identity"
    +
    +# create a managed identity
    +$ az identity create --resource-group "${IDENTITY_RESOURCE_GROUP}" --name "${IDENTITY_NAME}"
    +
    +

    Assign a role to the managed identity

    +

    Grant access to Azure DNS zone for the managed identity:

    +
    $ AZURE_DNS_ZONE_RESOURCE_GROUP="MyDnsResourceGroup" # name of resource group where dns zone is hosted
    +$ AZURE_DNS_ZONE="example.com" # DNS zone name like example.com or sub.example.com
    +
    +# fetch identity client id from managed identity created earlier
    +$ IDENTITY_CLIENT_ID=$(az identity show --resource-group "${IDENTITY_RESOURCE_GROUP}" \
    +  --name "${IDENTITY_NAME}" --query "clientId" --output tsv)
    +# fetch DNS id used to grant access to the managed identity
    +$ DNS_ID=$(az network dns zone show --name "${AZURE_DNS_ZONE}" \
    +  --resource-group "${AZURE_DNS_ZONE_RESOURCE_GROUP}" --query "id" --output tsv)
    +$ RESOURCE_GROUP_ID=$(az group show --name "${AZURE_DNS_ZONE_RESOURCE_GROUP}" --query "id" --output tsv)
    +
    +$ az role assignment create --role "DNS Zone Contributor" \
    +  --assignee "${IDENTITY_CLIENT_ID}" --scope "${DNS_ID}"
    +$ az role assignment create --role "Reader" \
    +  --assignee "${IDENTITY_CLIENT_ID}" --scope "${RESOURCE_GROUP_ID}"
    +
    +

    Create a federated identity credential

    +

    A binding between the managed identity and the ExternalDNS service account needs to be setup by creating a federated identity resource:

    +
    $ OIDC_ISSUER_URL="$(az aks show -n myAKSCluster -g myResourceGroup --query "oidcIssuerProfile.issuerUrl" -otsv)"
    +
    +$ az identity federated-credential create --name ${IDENTITY_NAME} --identity-name ${IDENTITY_NAME} --resource-group $AZURE_AKS_RESOURCE_GROUP} --issuer "$OIDC_ISSUER_URL" --subject "system:serviceaccount:default:external-dns"
    +
    +

    NOTE: make sure federated credential refers to correct namespace and service account (system:serviceaccount:<NAMESPACE>:<SERVICE_ACCOUNT>)

    +

    helm

    +

    When deploying external-dns with helm, here are the parameters you need to pass:

    +
    fullnameOverride: external-dns
    +
    +serviceAccount:
    +  annotations:
    +    azure.workload.identity/client-id: <IDENTITY_CLIENT_ID>
    +
    +podLabels:
    +  azure.workload.identity/use: "true"
    +
    +provider: azure
    +
    +secretConfiguration:
    +  enabled: true
    +  mountPath: "/etc/kubernetes/"
    +  data:
    +    azure.json: |
    +      {
    +        "subscriptionId": "<SUBSCRIPTION_ID>",
    +        "resourceGroup": "<AZURE_DNS_ZONE_RESOURCE_GROUP>",
    +        "useWorkloadIdentityExtension": true
    +      }
    +
    +

    NOTE: make sure the pod is restarted whenever you make a configuration change.

    +

    kubectl (alternative)

    +
    Create a configuration file for the managed identity
    +

    Create the file azure.json with the values from previous steps:

    +
    cat <<-EOF > /local/path/to/azure.json
    +{
    +  "subscriptionId": "$(az account show --query id -o tsv)",
    +  "resourceGroup": "$AZURE_DNS_ZONE_RESOURCE_GROUP",
    +  "useWorkloadIdentityExtension": true
    +}
    +EOF
    +
    +

    Use the azure.json file to create a Kubernetes secret:

    +
    $ kubectl create secret generic azure-config-file --namespace "default" --from-file /local/path/to/azure.json
    +
    +
    Update labels and annotations on ExternalDNS service account
    +

    To instruct Workload Identity webhook to inject a projected token into the ExternalDNS pod, the pod needs to have a label azure.workload.identity/use: "true" (before Workload Identity 1.0.0, this label was supposed to be set on the service account instead). Also, the service account needs to have an annotation azure.workload.identity/client-id: <IDENTITY_CLIENT_ID>:

    +

    To patch the existing serviceaccount and deployment, use the following command:

    +
    $ kubectl patch serviceaccount external-dns --namespace "default" --patch \
    + "{\"metadata\": {\"annotations\": {\"azure.workload.identity/client-id\": \"${IDENTITY_CLIENT_ID}\"}}}"
    +$ kubectl patch deployment external-dns --namespace "default" --patch \
    + '{"spec": {"template": {"metadata": {"labels": {\"azure.workload.identity/use\": \"true\"}}}}}'
    +
    +

    NOTE: it’s also possible to specify (or override) ClientID through UserAssignedIdentityID field in azure.json.

    +

    NOTE: make sure the pod is restarted whenever you make a configuration change.

    +

    Ingress used with ExternalDNS

    +

    This deployment assumes that you will be using nginx-ingress. When using nginx-ingress do not deploy it as a Daemon Set. This causes nginx-ingress to write the Cluster IP of the backend pods in the ingress status.loadbalancer.ip property which then has external-dns write the Cluster IP(s) in DNS vs. the nginx-ingress service external IP.

    +

    Ensure that your nginx-ingress deployment has the following arg: added to it:

    +
    - --publish-service=namespace/nginx-ingress-controller-svcname
    +
    +

    For more details see here: nginx-ingress external-dns

    +

    Deploy ExternalDNS

    +

    Connect your kubectl client to the cluster you want to test ExternalDNS with. Then apply one of the following manifests file to deploy ExternalDNS.

    +

    The deployment assumes that ExternalDNS will be installed into the default namespace. If this namespace is different, the ClusterRoleBinding will need to be updated to reflect the desired alternative namespace, such as external-dns, kube-addons, etc.

    +

    Manifest (for clusters without RBAC enabled)

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        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=MyDnsResourceGroup # (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
    +
    +

    Manifest (for clusters with RBAC enabled, cluster access)

    +
    apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRole
    +metadata:
    +  name: external-dns
    +rules:
    +  - apiGroups: [""]
    +    resources: ["services","endpoints","pods", "nodes"]
    +    verbs: ["get","watch","list"]
    +  - apiGroups: ["extensions","networking.k8s.io"]
    +    resources: ["ingresses"]
    +    verbs: ["get","watch","list"]
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRoleBinding
    +metadata:
    +  name: external-dns-viewer
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: ClusterRole
    +  name: external-dns
    +subjects:
    +  - kind: ServiceAccount
    +    name: external-dns
    +    namespace: default
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      serviceAccountName: external-dns
    +      containers:
    +        - name: external-dns
    +          image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +          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=MyDnsResourceGroup # (optional) use the DNS zones from the tutorial's resource group
    +            - --txt-prefix=externaldns-
    +          volumeMounts:
    +            - name: azure-config-file
    +              mountPath: /etc/kubernetes
    +              readOnly: true
    +      volumes:
    +        - name: azure-config-file
    +          secret:
    +            secretName: azure-config-file
    +
    +

    Manifest (for clusters with RBAC enabled, namespace access)

    +

    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 requires cluster access, so when using this manifest,
    +services with type NodePort will be skipped!

    +
    apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: Role
    +metadata:
    +  name: external-dns
    +rules:
    +  - apiGroups: [""]
    +    resources: ["services","endpoints","pods"]
    +    verbs: ["get","watch","list"]
    +  - apiGroups: ["extensions","networking.k8s.io"]
    +    resources: ["ingresses"]
    +    verbs: ["get","watch","list"]
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: RoleBinding
    +metadata:
    +  name: external-dns
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: Role
    +  name: external-dns
    +subjects:
    +  - kind: ServiceAccount
    +    name: external-dns
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      serviceAccountName: external-dns
    +      containers:
    +        - name: external-dns
    +          image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +          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=MyDnsResourceGroup # (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:

    +
    $ kubectl create --namespace "default" --filename externaldns.yaml
    +
    +

    Ingress Option: Expose an nginx service with an ingress

    +

    Create a file called nginx.yaml with the following contents:

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: nginx
    +spec:
    +  selector:
    +    matchLabels:
    +      app: nginx
    +  template:
    +    metadata:
    +      labels:
    +        app: nginx
    +    spec:
    +      containers:
    +        - image: nginx
    +          name: nginx
    +          ports:
    +          - containerPort: 80
    +---
    +apiVersion: v1
    +kind: Service
    +metadata:
    +  name: nginx-svc
    +spec:
    +  ports:
    +    - port: 80
    +      protocol: TCP
    +      targetPort: 80
    +  selector:
    +    app: nginx
    +  type: ClusterIP
    +
    +---
    +apiVersion: networking.k8s.io/v1
    +kind: Ingress
    +metadata:
    +  name: nginx
    +spec:
    +  ingressClassName: nginx
    +  rules:
    +    - host: server.example.com
    +      http:
    +        paths:
    +          - path: /
    +            pathType: Prefix
    +            backend:
    +              service:
    +                name: nginx-svc
    +                port:
    +                  number: 80
    +
    +

    When using ExternalDNS with ingress objects it will automatically create DNS records based on host names specified in ingress objects that match the domain-filter argument in the external-dns deployment manifest. When those host names are removed or renamed the corresponding DNS records are also altered.

    +

    Create the deployment, service and ingress object:

    +
    $ kubectl create --namespace "default" --filename nginx.yaml
    +
    +

    Since your external IP would have already been assigned to the nginx-ingress service, the DNS records pointing to the IP of the nginx-ingress service should be created within a minute.

    +

    Azure Load Balancer option: Expose an nginx service with a load balancer

    +

    Create a file called nginx.yaml with the following contents:

    +
    ---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: nginx
    +spec:
    +  selector:
    +    matchLabels:
    +      app: nginx
    +  template:
    +    metadata:
    +      labels:
    +        app: nginx
    +    spec:
    +      containers:
    +        - image: nginx
    +          name: nginx
    +          ports:
    +          - containerPort: 80
    +---
    +apiVersion: v1
    +kind: Service
    +annotations:
    +  external-dns.alpha.kubernetes.io/hostname: server.example.com
    +metadata:
    +  name: nginx-svc
    +spec:
    +  ports:
    +    - port: 80
    +      protocol: TCP
    +      targetPort: 80
    +  selector:
    +    app: nginx
    +  type: LoadBalancer
    +
    +

    The annotation external-dns.alpha.kubernetes.io/hostname is used to specify the DNS name that should be created for the service. The annotation value is a comma separated list of host names.

    +

    Verifying Azure DNS records

    +

    Run the following command to view the A records for your Azure DNS zone:

    +
    $ az network dns record-set a list --resource-group "MyDnsResourceGroup" --zone-name example.com
    +
    +

    Substitute the zone for the one created above if a different domain was used.

    +

    This should show the external IP address of the service as the A record for your domain (‘@’ indicates the record is for the zone itself).

    +

    Delete Azure Resource Group

    +

    Now that we have verified that ExternalDNS will automatically manage Azure DNS records, we can delete the tutorial’s
    +resource group:

    +
    $ az group delete --name "MyDnsResourceGroup"
    +
    +

    More tutorials

    +

    A video explanantion is available here: https://www.youtube.com/watch?v=VSn6DPKIhM8&list=PLpbcUe4chE79sB7Jg7B4z3HytqUUEwcNE

    +

    image

    + +
    +
    + + + Last update: + September 1, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/bluecat/index.html b/v0.13.6/tutorials/bluecat/index.html new file mode 100644 index 0000000000..03d12df99d --- /dev/null +++ b/v0.13.6/tutorials/bluecat/index.html @@ -0,0 +1,2255 @@ + + + + + + + + + + + + + + + + + + Setting up external-dns for BlueCat - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + +
    +
    +
    + + + +
    +
    +
    + + +
    +
    + + + + + + + + +

    Setting up external-dns for BlueCat

    +

    The first external-dns release with with BlueCat provider support is v0.8.0.

    +

    Prerequisites

    +

    Install the BlueCat Gateway product and deploy the community gateway workflows.

    +

    Configuration Options

    +

    There are two ways to pass configuration options to the Bluecat Provider JSON configuration file and command line flags. Currently if a valid configuration file is used all
    +BlueCat provider configurations will be taken from the configuration file. If a configuraiton file is not provided or cannot be read then all BlueCat provider configurations will
    +be taken from the command line flags. In the future an enhancement will be made to merge configuration options from the configuration file and command line flags if both are provided.

    +

    BlueCat provider supports getting the proxy URL from the environment variables. The format is the one specified by golang’s http.ProxyFromEnvironment.

    +

    Using CLI Flags

    +

    When using CLI flags to configure the Bluecat Provider the BlueCat Gateway credentials are passed in using environment variables BLUECAT_USERNAME and BLUECAT_PASSWORD.

    +

    Deploy

    +

    Setup up namespace, deployment, and service account:
    +

    kubectl create namespace bluecat-example
    +kubectl create secret generic bluecat-credentials --from-literal=username=bluecatuser --from-literal=password=bluecatpassword -n bluecat-example
    +cat << EOF > ~/bluecat.yml
    +---
    +apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  strategy:
    +    type: Recreate
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      serviceAccountName: external-dns
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --log-level=debug
    +        - --source=service
    +        - --provider=bluecat
    +        - --txt-owner-id=bluecat-example
    +        - --bluecat-dns-configuration=Example
    +        - --bluecat-dns-view=Internal
    +        - --bluecat-gateway-host=https://bluecatgw.example.com
    +        - --bluecat-root-zone=example.com
    +        env:
    +        - name: BLUECAT_USERNAME
    +          valueFrom:
    +            secretKeyRef:
    +              name: bluecat-credentials
    +              key: username
    +        - name: BLUECAT_PASSWORD
    +          valueFrom:
    +            secretKeyRef:
    +              name: bluecat-credentials
    +              key: password
    +EOF
    +kubectl apply -f ~/bluecat.yml -n bluecat-example
    +

    +

    Using JSON Configuration File

    +

    The options for configuring the Bluecat Provider are available through the JSON file provided to External-DNS via the flag --bluecat-config-file.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    KeyRequired
    gatewayHostYes
    gatewayUsernameNo
    gatewayPasswordNo
    dnsConfigurationYes
    dnsViewYes
    rootZoneYes
    dnsServerNameNo
    dnsDeployTypeNo
    skipTLSVerifyNo (default false)
    +

    Deploy

    +

    Setup configuration file as k8s Secret.
    +

    cat << EOF > ~/bluecat.json
    +{
    +  "gatewayHost": "https://bluecatgw.example.com",
    +  "gatewayUsername": "user",
    +  "gatewayPassword": "pass",
    +  "dnsConfiguration": "Example",
    +  "dnsView": "Internal",
    +  "rootZone": "example.com",
    +  "skipTLSVerify": false
    +}
    +EOF
    +kubectl create secret generic bluecatconfig --from-file ~/bluecat.json -n bluecat-example
    +

    +

    Setup up namespace, deployment, and service account:
    +

    kubectl create namespace bluecat-example
    +cat << EOF > ~/bluecat.yml
    +---
    +apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  strategy:
    +    type: Recreate
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      serviceAccountName: external-dns
    +      volumes:
    +        - name: bluecatconfig
    +          secret:
    +            secretName: bluecatconfig
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        volumeMounts:
    +          - name: bluecatconfig
    +            mountPath: "/etc/external-dns/"
    +            readOnly: true
    +        args:
    +        - --log-level=debug
    +        - --source=service
    +        - --provider=bluecat
    +        - --txt-owner-id=bluecat-example
    +        - --bluecat-config-file=/etc/external-dns/bluecat.json
    +EOF
    +kubectl apply -f ~/bluecat.yml -n bluecat-example
    +

    + +
    +
    + + + Last update: + May 29, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/civo/index.html b/v0.13.6/tutorials/civo/index.html new file mode 100644 index 0000000000..7f5e0f9969 --- /dev/null +++ b/v0.13.6/tutorials/civo/index.html @@ -0,0 +1,2239 @@ + + + + + + + + + + + + + + + + + + Setting up ExternalDNS for Services on Civo - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + + + + +
    +
    + + + + + + + + +

    Setting up ExternalDNS for Services on Civo

    +

    This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using Civo DNS Manager.

    +

    Make sure to use >0.13.5 version of ExternalDNS for this tutorial.

    +

    Managing DNS with Civo

    +

    If you want to learn about how to use Civo DNS Manager read the following tutorials:

    +

    An Introduction to Managing DNS

    +

    Get Civo Token

    +

    Copy the token in the settings for your account
    +The environment variable CIVO_TOKEN will be needed to run ExternalDNS with Civo.

    +

    Deploy ExternalDNS

    +

    Connect your kubectl client to the cluster you want to test ExternalDNS with.
    +Then apply one of the following manifests file to deploy ExternalDNS.

    +

    Manifest (for clusters without RBAC enabled)

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service # ingress is also possible
    +        - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
    +        - --provider=civo
    +        env:
    +        - name: CIVO_TOKEN
    +          value: "YOUR_CIVO_API_TOKEN"
    +
    +

    Manifest (for clusters with RBAC enabled)

    +
    apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRole
    +metadata:
    +  name: external-dns
    +rules:
    +- apiGroups: [""]
    +  resources: ["services","endpoints","pods"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: ["extensions","networking.k8s.io"]
    +  resources: ["ingresses"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: [""]
    +  resources: ["nodes"]
    +  verbs: ["list"]
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRoleBinding
    +metadata:
    +  name: external-dns-viewer
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: ClusterRole
    +  name: external-dns
    +subjects:
    +- kind: ServiceAccount
    +  name: external-dns
    +  namespace: default
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      serviceAccountName: external-dns
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service # ingress is also possible
    +        - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
    +        - --provider=civo
    +        env:
    +        - name: CIVO_TOKEN
    +          value: "YOUR_CIVO_API_TOKEN"
    +
    +

    Deploying an Nginx Service

    +

    Create a service file called ‘nginx.yaml’ with the following contents:

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: nginx
    +spec:
    +  selector:
    +    matchLabels:
    +      app: nginx
    +  template:
    +    metadata:
    +      labels:
    +        app: nginx
    +    spec:
    +      containers:
    +      - image: nginx
    +        name: nginx
    +        ports:
    +        - containerPort: 80
    +---
    +apiVersion: v1
    +kind: Service
    +metadata:
    +  name: nginx
    +  annotations:
    +    external-dns.alpha.kubernetes.io/hostname: my-app.example.com
    +spec:
    +  selector:
    +    app: nginx
    +  type: LoadBalancer
    +  ports:
    +    - protocol: TCP
    +      port: 80
    +      targetPort: 80
    +
    +

    Note the annotation on the service; use the same hostname as the Civo DNS zone created above.

    +

    ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records.

    +

    Create the deployment and service:

    +
    $ kubectl create -f nginx.yaml
    +
    +

    Depending where you run your service it can take a little while for your cloud provider to create an external IP for the service.

    +

    Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize the Civo DNS records.

    +

    Verifying Civo DNS records

    +

    Check your Civo UI to view the records for your Civo DNS zone.

    +

    Click on the zone for the one created above if a different domain was used.

    +

    This should show the external IP address of the service as the A record for your domain.

    +

    Cleanup

    +

    Now that we have verified that ExternalDNS will automatically manage Civo DNS records, we can delete the tutorial’s example:

    +
    $ kubectl delete service -f nginx.yaml
    +$ kubectl delete service -f externaldns.yaml
    +
    + +
    +
    + + + Last update: + June 14, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/cloudflare/index.html b/v0.13.6/tutorials/cloudflare/index.html new file mode 100644 index 0000000000..62e5955cfc --- /dev/null +++ b/v0.13.6/tutorials/cloudflare/index.html @@ -0,0 +1,2297 @@ + + + + + + + + + + + + + + + + + + Setting up ExternalDNS for Services on Cloudflare - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + + + + +
    +
    + + + + + + + + +

    Setting up ExternalDNS for Services on Cloudflare

    +

    This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using Cloudflare DNS.

    +

    Make sure to use >=0.4.2 version of ExternalDNS for this tutorial.

    +

    Creating a Cloudflare DNS zone

    +

    We highly recommend to read this tutorial if you haven’t used Cloudflare before:

    +

    Create a Cloudflare account and add a website

    +

    Creating Cloudflare Credentials

    +

    Snippet from Cloudflare - Getting Started:

    +
    +

    Cloudflare’s API exposes the entire Cloudflare infrastructure via a standardized programmatic interface. Using Cloudflare’s API, you can do just about anything you can do on cloudflare.com via the customer dashboard.

    +

    The Cloudflare API is a RESTful API based on HTTPS requests and JSON responses. If you are registered with Cloudflare, you can obtain your API key from the bottom of the “My Account” page, found here: Go to My account.

    +
    +

    API Token will be preferred for authentication if CF_API_TOKEN environment variable is set.
    +Otherwise CF_API_KEY and CF_API_EMAIL should be set to run ExternalDNS with Cloudflare.
    +You may provide the Cloudflare API token through a file by setting the
    +CF_API_TOKEN="file:/path/to/token".

    +

    When using API Token authentication, the token should be granted Zone Read, DNS Edit privileges, and access to All zones.

    +

    If you would like to further restrict the API permissions to a specific zone (or zones), you also need to use the --zone-id-filter so that the underlying API requests only access the zones that you explicitly specify, as opposed to accessing all zones.

    +

    Throttling

    +

    Cloudflare API has a global rate limit of 1,200 requests per five minutes. Running several fast polling ExternalDNS instances in a given account can easily hit that limit. The AWS Provider docs has some recommendations that can be followed here too, but in particular, consider passing --cloudflare-dns-records-per-page with a high value (maximum is 5,000).

    +

    Deploy ExternalDNS

    +

    Connect your kubectl client to the cluster you want to test ExternalDNS with.
    +Then apply one of the following manifests file to deploy ExternalDNS.

    +

    Manifest (for clusters without RBAC enabled)

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service # ingress is also possible
    +        - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
    +        - --zone-id-filter=023e105f4ecef8ad9ca31a8372d0c353 # (optional) limit to a specific zone.
    +        - --provider=cloudflare
    +        - --cloudflare-proxied # (optional) enable the proxy feature of Cloudflare (DDOS protection, CDN...)
    +        - --cloudflare-dns-records-per-page=5000 # (optional) configure how many DNS records to fetch per request
    +        env:
    +        - name: CF_API_KEY
    +          value: "YOUR_CLOUDFLARE_API_KEY"
    +        - name: CF_API_EMAIL
    +          value: "YOUR_CLOUDFLARE_EMAIL"
    +
    +

    Manifest (for clusters with RBAC enabled)

    +
    apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRole
    +metadata:
    +  name: external-dns
    +rules:
    +- apiGroups: [""]
    +  resources: ["services","endpoints","pods"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: ["extensions","networking.k8s.io"]
    +  resources: ["ingresses"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: [""]
    +  resources: ["nodes"]
    +  verbs: ["list", "watch"]
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRoleBinding
    +metadata:
    +  name: external-dns-viewer
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: ClusterRole
    +  name: external-dns
    +subjects:
    +- kind: ServiceAccount
    +  name: external-dns
    +  namespace: default
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      serviceAccountName: external-dns
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service # ingress is also possible
    +        - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
    +        - --zone-id-filter=023e105f4ecef8ad9ca31a8372d0c353 # (optional) limit to a specific zone.
    +        - --provider=cloudflare
    +        - --cloudflare-proxied # (optional) enable the proxy feature of Cloudflare (DDOS protection, CDN...)
    +        - --cloudflare-dns-records-per-page=5000 # (optional) configure how many DNS records to fetch per request
    +        env:
    +        - name: CF_API_KEY
    +          value: "YOUR_CLOUDFLARE_API_KEY"
    +        - name: CF_API_EMAIL
    +          value: "YOUR_CLOUDFLARE_EMAIL"
    +
    +

    Deploying an Nginx Service

    +

    Create a service file called ‘nginx.yaml’ with the following contents:

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: nginx
    +spec:
    +  selector:
    +    matchLabels:
    +      app: nginx
    +  template:
    +    metadata:
    +      labels:
    +        app: nginx
    +    spec:
    +      containers:
    +      - image: nginx
    +        name: nginx
    +        ports:
    +        - containerPort: 80
    +---
    +apiVersion: v1
    +kind: Service
    +metadata:
    +  name: nginx
    +  annotations:
    +    external-dns.alpha.kubernetes.io/hostname: example.com
    +    external-dns.alpha.kubernetes.io/ttl: "120" #optional
    +spec:
    +  selector:
    +    app: nginx
    +  type: LoadBalancer
    +  ports:
    +    - protocol: TCP
    +      port: 80
    +      targetPort: 80
    +
    +

    Note the annotation on the service; use the same hostname as the Cloudflare DNS zone created above. The annotation may also be a subdomain
    +of the DNS zone (e.g. ‘www.example.com’).

    +

    By setting the TTL annotation on the service, you have to pass a valid TTL, which must be 120 or above.
    +This annotation is optional, if you won’t set it, it will be 1 (automatic) which is 300.
    +For Cloudflare proxied entries, set the TTL annotation to 1 (automatic), or do not set it.

    +

    ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation
    +will cause ExternalDNS to remove the corresponding DNS records.

    +

    Create the deployment and service:

    +
    $ kubectl create -f nginx.yaml
    +
    +

    Depending where you run your service it can take a little while for your cloud provider to create an external IP for the service.

    +

    Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize
    +the Cloudflare DNS records.

    +

    Verifying Cloudflare DNS records

    +

    Check your Cloudflare dashboard to view the records for your Cloudflare DNS zone.

    +

    Substitute the zone for the one created above if a different domain was used.

    +

    This should show the external IP address of the service as the A record for your domain.

    +

    Cleanup

    +

    Now that we have verified that ExternalDNS will automatically manage Cloudflare DNS records, we can delete the tutorial’s example:

    +
    $ kubectl delete -f nginx.yaml
    +$ kubectl delete -f externaldns.yaml
    +
    +

    Setting cloudflare-proxied on a per-ingress basis

    +

    Using the external-dns.alpha.kubernetes.io/cloudflare-proxied: "true" annotation on your ingress, you can specify if the proxy feature of Cloudflare should be enabled for that record. This setting will override the global --cloudflare-proxied setting.

    + +
    +
    + + + Last update: + May 29, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/contour/index.html b/v0.13.6/tutorials/contour/index.html new file mode 100644 index 0000000000..a187812436 --- /dev/null +++ b/v0.13.6/tutorials/contour/index.html @@ -0,0 +1,2243 @@ + + + + + + + + + + + + + + + + + + Setting up External DNS with Contour - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + +
    +
    +
    + + + +
    +
    +
    + + +
    +
    + + + + + + + + +

    Setting up External DNS with Contour

    +

    This tutorial describes how to configure External DNS to use the Contour HTTPProxy source.
    +Using the HTTPProxy resource with External DNS requires Contour version 1.5 or greater.

    +

    Example manifests for External DNS

    +

    Without RBAC

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service
    +        - --source=ingress
    +        - --source=contour-httpproxy
    +        - --domain-filter=external-dns-test.my-org.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones
    +        - --provider=aws
    +        - --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization
    +        - --aws-zone-type=public # only look at public hosted zones (valid values are public, private or no value for both)
    +        - --registry=txt
    +        - --txt-owner-id=my-identifier
    +
    +

    With RBAC

    +
    apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRole
    +metadata:
    +  name: external-dns
    +rules:
    +- apiGroups: [""]
    +  resources: ["services","endpoints","pods"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: ["extensions","networking.k8s.io"]
    +  resources: ["ingresses"] 
    +  verbs: ["get","watch","list"]
    +- apiGroups: [""]
    +  resources: ["nodes"]
    +  verbs: ["list"]
    +- apiGroups: ["projectcontour.io"]
    +  resources: ["httpproxies"]
    +  verbs: ["get","watch","list"]
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRoleBinding
    +metadata:
    +  name: external-dns-viewer
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: ClusterRole
    +  name: external-dns
    +subjects:
    +- kind: ServiceAccount
    +  name: external-dns
    +  namespace: default
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      serviceAccountName: external-dns
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service
    +        - --source=ingress
    +        - --source=contour-httpproxy
    +        - --domain-filter=external-dns-test.my-org.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones
    +        - --provider=aws
    +        - --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization
    +        - --aws-zone-type=public # only look at public hosted zones (valid values are public, private or no value for both)
    +        - --registry=txt
    +        - --txt-owner-id=my-identifier
    +
    +

    Verify External DNS works

    +

    The following instructions are based on the
    +Contour example workload.

    +

    Install a sample service

    +
    $ kubectl apply -f - <<EOF
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  labels:
    +    app: kuard
    +  name: kuard
    +spec:
    +  replicas: 3
    +  selector:
    +    matchLabels:
    +      app: kuard
    +  template:
    +    metadata:
    +      labels:
    +        app: kuard
    +    spec:
    +      containers:
    +      - image: gcr.io/kuar-demo/kuard-amd64:1
    +        name: kuard
    +---
    +apiVersion: v1
    +kind: Service
    +metadata:
    +  labels:
    +    app: kuard
    +  name: kuard
    +spec:
    +  ports:
    +  - port: 80
    +    protocol: TCP
    +    targetPort: 8080
    +  selector:
    +    app: kuard
    +  sessionAffinity: None
    +  type: ClusterIP
    +EOF
    +
    +

    Then create an HTTPProxy:

    +
    $ kubectl apply -f - <<EOF
    +apiVersion: projectcontour.io/v1
    +kind: HTTPProxy
    +metadata:
    +  labels:
    +    app: kuard
    +  name: kuard
    +  namespace: default
    +spec:
    +  virtualhost:
    +    fqdn: kuard.example.com
    +  routes:
    +    - conditions:
    +      - prefix: /
    +      services:
    +        - name: kuard
    +          port: 80
    +EOF
    +
    +

    Access the sample service using curl

    +
    $ curl -i http://kuard.example.com/healthy
    +HTTP/1.1 200 OK
    +Content-Type: text/plain
    +Date: Thu, 27 Jun 2019 19:42:26 GMT
    +Content-Length: 2
    +
    +ok
    +
    + +
    +
    + + + Last update: + July 4, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/coredns/index.html b/v0.13.6/tutorials/coredns/index.html new file mode 100644 index 0000000000..25d0928ffa --- /dev/null +++ b/v0.13.6/tutorials/coredns/index.html @@ -0,0 +1,2355 @@ + + + + + + + + + + + + + + + + + + Setting up ExternalDNS for CoreDNS with minikube - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + + + + +
    +
    + + + + + + + + +

    Setting up ExternalDNS for CoreDNS with minikube

    +

    This tutorial describes how to setup ExternalDNS for usage within a minikube cluster that makes use of CoreDNS and nginx ingress controller.
    +You need to:
    +* install CoreDNS with etcd enabled
    +* install external-dns with coredns as a provider
    +* enable ingress controller for the minikube cluster

    +

    Creating a cluster

    +
    minikube start
    +
    +

    Installing CoreDNS with etcd enabled

    +

    Helm chart is used to install etcd and CoreDNS.

    +

    Initializing helm chart

    +
    helm init
    +
    +

    Installing etcd

    +

    etcd operator is used to manage etcd clusters.
    +

    helm install stable/etcd-operator --name my-etcd-op
    +

    +etcd cluster is installed with example yaml from etcd operator website.
    +
    kubectl apply -f https://raw.githubusercontent.com/coreos/etcd-operator/HEAD/example/example-etcd-cluster.yaml
    +

    +

    Installing CoreDNS

    +

    In order to make CoreDNS work with etcd backend, values.yaml of the chart should be changed with corresponding configurations.
    +

    wget https://raw.githubusercontent.com/helm/charts/HEAD/stable/coredns/values.yaml
    +

    +

    You need to edit/patch the file with below diff
    +

    diff --git a/values.yaml b/values.yaml
    +index 964e72b..e2fa934 100644
    +--- a/values.yaml
    ++++ b/values.yaml
    +@@ -27,12 +27,12 @@ service:
    +
    + rbac:
    +   # If true, create & use RBAC resources
    +-  create: false
    ++  create: true
    +   # Ignored if rbac.create is true
    +   serviceAccountName: default
    +
    + # isClusterService specifies whether chart should be deployed as cluster-service or normal k8s app.
    +-isClusterService: true
    ++isClusterService: false
    +
    + servers:
    + - zones:
    +@@ -51,6 +51,12 @@ servers:
    +     parameters: 0.0.0.0:9153
    +   - name: proxy
    +     parameters: . /etc/resolv.conf
    ++  - name: etcd
    ++    parameters: example.org
    ++    configBlock: |-
    ++      stubzones
    ++      path /skydns
    ++      endpoint http://10.105.68.165:2379
    +
    + # Complete example with all the options:
    + # - zones:                 # the `zones` block can be left out entirely, defaults to "."
    +

    +Note:
    +* IP address of etcd’s endpoint should be get from etcd client service. It should be “example-etcd-cluster-client” in this example. This IP address is used through this document for etcd endpoint configuration.
    +
    $ kubectl get svc example-etcd-cluster-client
    +NAME                          TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
    +example-etcd-cluster-client   ClusterIP   10.105.68.165   <none>        2379/TCP   16m
    +

    +* Parameters should configure your own domain. “example.org” is used in this example.

    +

    After configuration done in values.yaml, you can install coredns chart.
    +

    helm install --name my-coredns --values values.yaml stable/coredns
    +

    +

    Installing ExternalDNS

    +

    Install external ExternalDNS

    +

    ETCD_URLS is configured to etcd client service address.

    +

    Manifest (for clusters without RBAC enabled)

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +  namespace: kube-system
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=ingress
    +        - --provider=coredns
    +        - --log-level=debug # debug only
    +        env:
    +        - name: ETCD_URLS
    +          value: http://10.105.68.165:2379
    +
    +

    Manifest (for clusters with RBAC enabled)

    +
    ---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRole
    +metadata:
    +  name: external-dns
    +rules:
    +- apiGroups: [""]
    +  resources: ["services","endpoints","pods"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: ["extensions","networking.k8s.io"]
    +  resources: ["ingresses"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: [""]
    +  resources: ["nodes"]
    +  verbs: ["list"]
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRoleBinding
    +metadata:
    +  name: external-dns-viewer
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: ClusterRole
    +  name: external-dns
    +subjects:
    +- kind: ServiceAccount
    +  name: external-dns
    +  namespace: kube-system
    +---
    +apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +  namespace: kube-system
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +  namespace: kube-system
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      serviceAccountName: external-dns
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=ingress
    +        - --provider=coredns
    +        - --log-level=debug # debug only
    +        env:
    +        - name: ETCD_URLS
    +          value: http://10.105.68.165:2379
    +
    +

    Enable the ingress controller

    +

    You can use the ingress controller in minikube cluster. It needs to enable ingress addon in the cluster.
    +

    minikube addons enable ingress
    +

    +

    Testing ingress example

    +
    $ cat ingress.yaml
    +apiVersion: networking.k8s.io/v1
    +kind: Ingress
    +metadata:
    +  name: nginx
    +spec:
    +  ingressClassName: nginx
    +  rules:
    +  - host: nginx.example.org
    +    http:
    +      paths:
    +      - backend:
    +          serviceName: nginx
    +          servicePort: 80
    +
    +$ kubectl apply -f ingress.yaml
    +ingress.extensions "nginx" created
    +
    +

    Wait a moment until DNS has the ingress IP. The DNS service IP is from CoreDNS service. It is “my-coredns-coredns” in this example.
    +

    $ kubectl get svc my-coredns-coredns
    +NAME                 TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
    +my-coredns-coredns   ClusterIP   10.100.4.143   <none>        53/UDP    12m
    +
    +$ kubectl get ingress
    +NAME      HOSTS               ADDRESS     PORTS     AGE
    +nginx     nginx.example.org   10.0.2.15   80        2m
    +
    +$ kubectl run -it --rm --restart=Never --image=infoblox/dnstools:latest dnstools
    +If you don't see a command prompt, try pressing enter.
    +dnstools# dig @10.100.4.143 nginx.example.org +short
    +10.0.2.15
    +dnstools#
    +

    + +
    +
    + + + Last update: + May 29, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/designate/index.html b/v0.13.6/tutorials/designate/index.html new file mode 100644 index 0000000000..8240da01a3 --- /dev/null +++ b/v0.13.6/tutorials/designate/index.html @@ -0,0 +1,2322 @@ + + + + + + + + + + + + + + + + + + Setting up ExternalDNS for Services on OpenStack Designate - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + + + + +
    +
    + + + + + + + + +

    Setting up ExternalDNS for Services on OpenStack Designate

    +

    This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using OpenStack Designate DNS.

    +

    Authenticating with OpenStack

    +

    We are going to use OpenStack CLI - openstack utility, which is an umbrella application for most of OpenStack clients including designate.

    +

    All OpenStack CLIs require authentication parameters to be provided. These parameters include:
    +* URL of the OpenStack identity service (keystone) which is responsible for user authentication and also served as a registry for other
    + OpenStack services. Designate endpoints must be registered in keystone in order to ExternalDNS and OpenStack CLI be able to find them.
    +* OpenStack region name
    +* User login name.
    +* User project (tenant) name.
    +* User domain (only when using keystone API v3)

    +

    Although these parameters can be passed explicitly through the CLI flags, traditionally it is done by sourcing openrc file (source ~/openrc) that is a
    +shell snippet that sets environment variables that all OpenStack CLI understand by convention.

    +

    Recent versions of OpenStack Dashboard have a nice UI to download openrc file for both v2 and v3 auth protocols. Both protocols can be used with ExternalDNS.
    +v3 is generally preferred over v2, but might not be available in some OpenStack installations.

    +

    Installing OpenStack Designate

    +

    Please refer to the Designate deployment tutorial for instructions on how
    +to install and test Designate with BIND backend. You will be required to have admin rights in existing OpenStack installation to do this. One convenient
    +way to get yourself an OpenStack installation to play with is to use DevStack.

    +

    Creating DNS zones

    +

    All domain names that are ExternalDNS is going to create must belong to one of DNS zones created in advance. Here is an example of how to create example.com DNS zone:
    +

    $ openstack zone create --email dnsmaster@example.com example.com.
    +

    +

    It is important to manually create all the zones that are going to be used for kubernetes entities (ExternalDNS sources) before starting ExternalDNS.

    +

    Deploy ExternalDNS

    +

    Create a deployment file called externaldns.yaml with the following contents:

    +

    Manifest (for clusters without RBAC enabled)

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service # ingress is also possible
    +        - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
    +        - --provider=designate
    +        env: # values from openrc file
    +        - name: OS_AUTH_URL
    +          value: https://controller/identity/v3
    +        - name: OS_REGION_NAME
    +          value: RegionOne
    +        - name: OS_USERNAME
    +          value: admin
    +        - name: OS_PASSWORD
    +          value: p@ssw0rd
    +        - name: OS_PROJECT_NAME
    +          value: demo
    +        - name: OS_USER_DOMAIN_NAME
    +          value: Default
    +
    +

    Manifest (for clusters with RBAC enabled)

    +
    apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRole
    +metadata:
    +  name: external-dns
    +rules:
    +- apiGroups: [""]
    +  resources: ["services","endpoints","pods"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: [""]
    +  resources: ["pods"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: ["extensions","networking.k8s.io"]
    +  resources: ["ingresses"] 
    +  verbs: ["get","watch","list"]
    +- apiGroups: [""]
    +  resources: ["nodes"]
    +  verbs: ["watch","list"]
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRoleBinding
    +metadata:
    +  name: external-dns-viewer
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: ClusterRole
    +  name: external-dns
    +subjects:
    +- kind: ServiceAccount
    +  name: external-dns
    +  namespace: default
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  strategy:
    +    type: Recreate
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      serviceAccountName: external-dns
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service # ingress is also possible
    +        - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
    +        - --provider=designate
    +        env: # values from openrc file
    +        - name: OS_AUTH_URL
    +          value: https://controller/identity/v3
    +        - name: OS_REGION_NAME
    +          value: RegionOne
    +        - name: OS_USERNAME
    +          value: admin
    +        - name: OS_PASSWORD
    +          value: p@ssw0rd
    +        - name: OS_PROJECT_NAME
    +          value: demo
    +        - name: OS_USER_DOMAIN_NAME
    +          value: Default
    +
    +

    Create the deployment for ExternalDNS:

    +
    $ kubectl create -f externaldns.yaml
    +
    +

    Optional: Trust self-sign certificates

    +

    If your OpenStack-Installation is configured with a self-sign certificate, you could extend the pod.spec with following secret-mount:
    +

            volumeMounts:
    +        - mountPath: /etc/ssl/certs/
    +          name: cacerts 
    +      volumes:
    +      - name: cacerts
    +        secret:
    +          defaultMode: 420
    +          secretName: self-sign-certs
    +

    +

    content of the secret self-sign-certs must be the certificate/chain in PEM format.

    +

    Deploying an Nginx Service

    +

    Create a service file called ‘nginx.yaml’ with the following contents:

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: nginx
    +spec:
    +  selector:
    +    matchLabels:
    +      app: nginx
    +  template:
    +    metadata:
    +      labels:
    +        app: nginx
    +    spec:
    +      containers:
    +      - image: nginx
    +        name: nginx
    +        ports:
    +        - containerPort: 80
    +---
    +apiVersion: v1
    +kind: Service
    +metadata:
    +  name: nginx
    +  annotations:
    +    external-dns.alpha.kubernetes.io/hostname: my-app.example.com
    +spec:
    +  selector:
    +    app: nginx
    +  type: LoadBalancer
    +  ports:
    +    - protocol: TCP
    +      port: 80
    +      targetPort: 80
    +
    +

    Note the annotation on the service; use the same hostname as the DNS zone created above.

    +

    ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records.

    +

    Create the deployment and service:

    +
    $ kubectl create -f nginx.yaml
    +
    +

    Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and notify Designate,
    +which in turn synchronize DNS records with underlying DNS server backend.

    +

    Verifying DNS records

    +

    To verify that DNS record was indeed created, you can use the following command:

    +
    $ openstack recordset list example.com.
    +
    +

    There should be a record for my-app.example.com having ACTIVE status. And of course, the ultimate method to verify is to issue a DNS query:

    +
    $ dig my-app.example.com @controller
    +
    +

    Cleanup

    +

    Now that we have verified that ExternalDNS created all DNS records, we can delete the tutorial’s example:

    +
    $ kubectl delete service -f nginx.yaml
    +$ kubectl delete service -f externaldns.yaml
    +
    + +
    +
    + + + Last update: + May 29, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/digitalocean/index.html b/v0.13.6/tutorials/digitalocean/index.html new file mode 100644 index 0000000000..2e297e593b --- /dev/null +++ b/v0.13.6/tutorials/digitalocean/index.html @@ -0,0 +1,2290 @@ + + + + + + + + + + + + + + + + + + Setting up ExternalDNS for Services on DigitalOcean - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + + + + +
    +
    + + + + + + + + +

    Setting up ExternalDNS for Services on DigitalOcean

    +

    This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using DigitalOcean DNS.

    +

    Make sure to use >=0.4.2 version of ExternalDNS for this tutorial.

    +

    Creating a DigitalOcean DNS zone

    +

    If you want to learn about how to use DigitalOcean’s DNS service read the following tutorial series:

    +

    An Introduction to Managing DNS, and specifically How To Set Up a Host Name with DigitalOcean DNS

    +

    Create a new DNS zone where you want to create your records in. Let’s use example.com as an example here.

    +

    Creating DigitalOcean Credentials

    +

    Generate a new personal token by going to the API settings or follow How To Use the DigitalOcean API v2 if you need more information. Give the token a name and choose read and write access. The token needs to be passed to ExternalDNS so make a note of it for later use.

    +

    The environment variable DO_TOKEN will be needed to run ExternalDNS with DigitalOcean.

    +

    Deploy ExternalDNS

    +

    Connect your kubectl client to the cluster you want to test ExternalDNS with.
    +Then apply one of the following manifests file to deploy ExternalDNS.

    +

    Manifest (for clusters without RBAC enabled)

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  replicas: 1
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  strategy:
    +    type: Recreate
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service # ingress is also possible
    +        - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
    +        - --provider=digitalocean
    +        env:
    +        - name: DO_TOKEN
    +          value: "YOUR_DIGITALOCEAN_API_KEY"
    +
    +

    Manifest (for clusters with RBAC enabled)

    +
    apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRole
    +metadata:
    +  name: external-dns
    +rules:
    +- apiGroups: [""]
    +  resources: ["services","endpoints","pods"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: ["extensions","networking.k8s.io"]
    +  resources: ["ingresses"] 
    +  verbs: ["get","watch","list"]
    +- apiGroups: [""]
    +  resources: ["nodes"]
    +  verbs: ["list"]
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRoleBinding
    +metadata:
    +  name: external-dns-viewer
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: ClusterRole
    +  name: external-dns
    +subjects:
    +- kind: ServiceAccount
    +  name: external-dns
    +  namespace: default
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  replicas: 1
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  strategy:
    +    type: Recreate
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      serviceAccountName: external-dns
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service # ingress is also possible
    +        - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
    +        - --provider=digitalocean
    +        env:
    +        - name: DO_TOKEN
    +          value: "YOUR_DIGITALOCEAN_API_KEY"
    +
    +

    Deploying an Nginx Service

    +

    Create a service file called ‘nginx.yaml’ with the following contents:

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: nginx
    +spec:
    +  replicas: 1
    +  selector:
    +    matchLabels:
    +      app: nginx
    +  template:
    +    metadata:
    +      labels:
    +        app: nginx
    +    spec:
    +      containers:
    +      - image: nginx
    +        name: nginx
    +        ports:
    +        - containerPort: 80
    +---
    +apiVersion: v1
    +kind: Service
    +metadata:
    +  name: nginx
    +  annotations:
    +    external-dns.alpha.kubernetes.io/hostname: my-app.example.com
    +spec:
    +  selector:
    +    app: nginx
    +  type: LoadBalancer
    +  ports:
    +    - protocol: TCP
    +      port: 80
    +      targetPort: 80
    +
    +

    Note the annotation on the service; use the same hostname as the DigitalOcean DNS zone created above.

    +

    ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records.

    +

    Create the deployment and service:

    +
    $ kubectl create -f nginx.yaml
    +
    +

    Depending where you run your service it can take a little while for your cloud provider to create an external IP for the service.

    +

    Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize the DigitalOcean DNS records.

    +

    Verifying DigitalOcean DNS records

    +

    Check your DigitalOcean UI to view the records for your DigitalOcean DNS zone.

    +

    Click on the zone for the one created above if a different domain was used.

    +

    This should show the external IP address of the service as the A record for your domain.

    +

    Cleanup

    +

    Now that we have verified that ExternalDNS will automatically manage DigitalOcean DNS records, we can delete the tutorial’s example:

    +
    $ kubectl delete service -f nginx.yaml
    +$ kubectl delete service -f externaldns.yaml
    +
    +

    Advanced Usage

    +

    API Page Size

    +

    If you have a large number of domains and/or records within a domain, you may encounter API
    +rate limiting because of the number of API calls that external-dns must make to the DigitalOcean API to retrieve
    +the current DNS configuration during every reconciliation loop. If this is the case, use the
    +--digitalocean-api-page-size option to increase the size of the pages used when querying the DigitalOcean API.
    +(Note: external-dns uses a default of 50.)

    + +
    +
    + + + Last update: + May 29, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/dnsimple/index.html b/v0.13.6/tutorials/dnsimple/index.html new file mode 100644 index 0000000000..6cf50d6ff1 --- /dev/null +++ b/v0.13.6/tutorials/dnsimple/index.html @@ -0,0 +1,2332 @@ + + + + + + + + + + + + + + + + + + Setting up ExternalDNS for Services on DNSimple - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + + + + +
    +
    + + + + + + + + +

    Setting up ExternalDNS for Services on DNSimple

    +

    This tutorial describes how to setup ExternalDNS for usage with DNSimple.

    +

    Make sure to use >=0.4.6 version of ExternalDNS for this tutorial.

    +

    Created a DNSimple API Access Token

    +

    A DNSimple API access token can be acquired by following the provided documentation from DNSimple

    +

    The environment variable DNSIMPLE_OAUTH must be set to the API token generated for to run ExternalDNS with DNSimple.

    +

    Deploy ExternalDNS

    +

    Connect your kubectl client to the cluster you want to test ExternalDNS with.
    +Then apply one of the following manifests file to deploy ExternalDNS.

    +

    Manifest (for clusters without RBAC enabled)

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service
    +        - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone you create in DNSimple.
    +        - --provider=dnsimple
    +        - --registry=txt
    +        env:
    +        - name: DNSIMPLE_OAUTH
    +          value: "YOUR_DNSIMPLE_API_KEY"
    +
    +

    Manifest (for clusters with RBAC enabled)

    +
    apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRole
    +metadata:
    +  name: external-dns
    +rules:
    +- apiGroups: [""]
    +  resources: ["services","endpoints","pods"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: ["extensions","networking.k8s.io"]
    +  resources: ["ingresses"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: [""]
    +  resources: ["nodes"]
    +  verbs: ["list"]
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRoleBinding
    +metadata:
    +  name: external-dns-viewer
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: ClusterRole
    +  name: external-dns
    +subjects:
    +- kind: ServiceAccount
    +  name: external-dns
    +  namespace: default
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      serviceAccountName: external-dns
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service
    +        - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone you create in DNSimple.
    +        - --provider=dnsimple
    +        - --registry=txt
    +        env:
    +        - name: DNSIMPLE_OAUTH
    +          value: "YOUR_DNSIMPLE_API_KEY"
    +
    +

    Deploying an Nginx Service

    +

    Create a service file called ‘nginx.yaml’ with the following contents:

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: nginx
    +spec:
    +  selector:
    +    matchLabels:
    +      app: nginx
    +  template:
    +    metadata:
    +      labels:
    +        app: nginx
    +    spec:
    +      containers:
    +      - image: nginx
    +        name: nginx
    +        ports:
    +        - containerPort: 80
    +---
    +apiVersion: v1
    +kind: Service
    +metadata:
    +  name: nginx
    +  annotations:
    +    external-dns.alpha.kubernetes.io/hostname: validate-external-dns.example.com
    +spec:
    +  selector:
    +    app: nginx
    +  type: LoadBalancer
    +  ports:
    +    - protocol: TCP
    +      port: 80
    +      targetPort: 80
    +
    +

    Note the annotation on the service; use the same hostname as the DNSimple DNS zone created above. The annotation may also be a subdomain
    +of the DNS zone (e.g. ‘www.example.com’).

    +

    ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records.

    +

    Create the deployment and service:

    +
    $ kubectl create -f nginx.yaml
    +
    +

    Depending where you run your service it can take a little while for your cloud provider to create an external IP for the service. Check the status by running
    +kubectl get services nginx. If the EXTERNAL-IP field shows an address, the service is ready to be accessed externally.

    +

    Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize
    +the DNSimple DNS records.

    +

    Verifying DNSimple DNS records

    +

    Getting your DNSimple Account ID

    +

    If you do not know your DNSimple account ID it can be acquired using the whoami endpoint from the DNSimple Identity API

    +
    curl -H "Authorization: Bearer $DNSIMPLE_ACCOUNT_TOKEN" \
    +    -H 'Accept: application/json' \
    +    https://api.dnsimple.com/v2/whoami
    +{
    +  "data": {
    +    "user": null,
    +    "account": {
    +      "id": 1,
    +      "email": "example-account@example.com",
    +      "plan_identifier": "dnsimple-professional",
    +      "created_at": "2015-09-18T23:04:37Z",
    +      "updated_at": "2016-06-09T20:03:39Z"
    +    }
    +  }
    +}
    +
    +

    Looking at the DNSimple Dashboard

    +

    You can view your DNSimple Record Editor at https://dnsimple.com/a/YOUR_ACCOUNT_ID/domains/example.com/records. Ensure you substitute the value YOUR_ACCOUNT_ID with the ID of your DNSimple account and example.com with the correct domain that you used during validation.

    +

    Using the DNSimple Zone Records API

    +

    This approach allows for you to use the DNSimple List records for a zone endpoint to verify the creation of the A and TXT record. Ensure you substitute the value YOUR_ACCOUNT_ID with the ID of your DNSimple account and example.com with the correct domain that you used during validation.

    +
    curl -H "Authorization: Bearer $DNSIMPLE_ACCOUNT_TOKEN" \
    +    -H 'Accept: application/json' \
    +    'https://api.dnsimple.com/v2/YOUR_ACCOUNT_ID/zones/example.com/records&name=validate-external-dns'
    +
    +

    Clean up

    +

    Now that we have verified that ExternalDNS will automatically manage DNSimple DNS records, we can delete the tutorial’s example:

    +
    $ kubectl delete -f nginx.yaml
    +$ kubectl delete -f externaldns.yaml
    +
    +

    Deleting Created Records

    +

    The created records can be deleted using the record IDs from the verification step and the Delete a zone record endpoint.

    + +
    +
    + + + Last update: + May 29, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/dyn/index.html b/v0.13.6/tutorials/dyn/index.html new file mode 100644 index 0000000000..1811b10853 --- /dev/null +++ b/v0.13.6/tutorials/dyn/index.html @@ -0,0 +1,2164 @@ + + + + + + + + + + + + + + + + + + Setting up ExternalDNS for Dyn - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + + + + +
    +
    + + + + + + + + +

    Setting up ExternalDNS for Dyn

    +

    Creating a Dyn Configuration Secret

    +

    For ExternalDNS to access the Dyn API, create a Kubernetes secret.

    +

    To create the secret:

    +
    $ kubectl create secret generic external-dns \
    +      --from-literal=EXTERNAL_DNS_DYN_CUSTOMER_NAME=${DYN_CUSTOMER_NAME} \
    +      --from-literal=EXTERNAL_DNS_DYN_USERNAME=${DYN_USERNAME} \
    +      --from-literal=EXTERNAL_DNS_DYN_PASSWORD=${DYN_PASSWORD}
    +
    +

    The credentials are the same ones created during account registration. As best practise, you are advised to
    +create an API-only user that is entitled to only the zones intended to be changed by ExternalDNS

    +

    Deploy ExternalDNS

    +

    The rest of this tutorial assumes you own example.com domain and your DNS provider is Dyn. Change example.com
    +with a domain/zone that you really own.

    +

    In case of the dyn provider, the flag --zone-id-filter is mandatory as it specifies which zones to scan for records. Without it

    +

    Create a deployment file called externaldns.yaml with the following contents:

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=ingress
    +        - --txt-prefix=_d
    +        - --namespace=example
    +        - --zone-id-filter=example.com
    +        - --domain-filter=example.com
    +        - --provider=dyn
    +        env:
    +        - name: EXTERNAL_DNS_DYN_CUSTOMER_NAME
    +          valueFrom:
    +            secretKeyRef:
    +              name: external-dns
    +              key: EXTERNAL_DNS_DYN_CUSTOMER_NAME
    +        - name: EXTERNAL_DNS_DYN_USERNAME
    +          valueFrom:
    +            secretKeyRef:
    +              name: external-dns
    +              key: EXTERNAL_DNS_DYN_USERNAME
    +        - name: EXTERNAL_DNS_DYN_PASSWORD
    +          valueFrom:
    +            secretKeyRef:
    +              name: external-dns
    +              key: EXTERNAL_DNS_DYN_PASSWORD
    +EOF
    +
    +

    As we’ll be creating an Ingress resource, you need --txt-prefix=_d as a CNAME cannot coexist with a TXT record. You can change the prefix to
    +any valid start of a FQDN.

    +

    Create the deployment for ExternalDNS:

    +
    $ kubectl create -f externaldns.yaml
    +
    +

    Running a locally build version

    +

    If you just want to test ExternalDNS in dry-run mode locally without doing the above deployment you can also do it.
    +Make sure your kubectl is configured correctly . Assuming you have the sources, build and run it like so:

    +
    make 
    +# output skipped
    +
    +./build/external-dns \
    +    --provider=dyn \
    +    --dyn-customer-name=${DYN_CUSTOMER_NAME} \
    +    --dyn-username=${DYN_USERNAME} \
    +    --dyn-password=${DYN_PASSWORD} \
    +    --domain-filter=example.com \
    +    --zone-id-filter=example.com \
    +    --namespace=example \
    +    --log-level=debug \
    +    --txt-prefix=_ \
    +    --dry-run=true
    +INFO[0000] running in dry-run mode. No changes to DNS records will be made. 
    +INFO[0000] Connected to cluster at https://some-k8s-cluster.example.com 
    +INFO[0001] Zones: [example.com]
    +# output skipped
    +
    +

    Having --dry-run=true and --log-level=debug is a great way to see exactly what DynamicDNS is doing or is about to do.

    +

    Deploying an Ingress Resource

    +

    Create a file called ‘test-ingress.yaml’ with the following contents:

    +
    apiVersion: networking.k8s.io/v1
    +kind: Ingress
    +metadata:  
    +  name: test-ingress
    +  namespace: example
    +spec:
    +  rules:
    +  - host: test-ingress.example.com
    +    http:
    +      paths:
    +      - backend:
    +          service:
    +            name: my-awesome-service
    +            port:
    +              number: 8080
    +        pathType: Prefix
    +
    +

    As the DNS name test-ingress.example.com matches the filter, external-dns will create two records:
    +a CNAME for test-ingress.example.com and TXT for _dtest-ingress.example.com.

    +

    Create the Ingress:

    +
    $ kubectl create -f test-ingress.yaml
    +
    +

    By default external-dns scans for changes every minute so give it some time to catch up with the

    +

    Verifying Dyn DNS records

    +

    Login to the console at https://portal.dynect.net/login/ and verify records are created

    +

    Clean up

    +

    Login to the console at https://portal.dynect.net/login/ and delete the records created. Alternatively, just delete the sample
    +Ingress resources and external-dns will delete the records.

    + +
    +
    + + + Last update: + May 29, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/exoscale/index.html b/v0.13.6/tutorials/exoscale/index.html new file mode 100644 index 0000000000..32c0418909 --- /dev/null +++ b/v0.13.6/tutorials/exoscale/index.html @@ -0,0 +1,2167 @@ + + + + + + + + + + + + + + + + + + Setting up ExternalDNS for Exoscale - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + +
    +
    +
    + + + +
    +
    +
    + + +
    +
    + + + + + + + + +

    Setting up ExternalDNS for Exoscale

    +

    Prerequisites

    +

    Exoscale provider support was added via this PR, thus you need to use external-dns v0.5.5.

    +

    The Exoscale provider expects that your Exoscale zones, you wish to add records to, already exists
    +and are configured correctly. It does not add, remove or configure new zones in anyway.

    +

    To do this please refer to the Exoscale DNS documentation.

    +

    Additionally you will have to provide the Exoscale…:

    +
      +
    • API Key
    • +
    • API Secret
    • +
    • Elastic IP address, to access the workers
    • +
    +

    Deployment

    +

    Deploying external DNS for Exoscale is actually nearly identical to deploying
    +it for other providers. This is what a sample deployment.yaml looks like:

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      # Only use if you're also using RBAC
    +      # serviceAccountName: external-dns
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=ingress # or service or both
    +        - --provider=exoscale
    +        - --domain-filter={{ my-domain }}
    +        - --policy=sync # if you want DNS entries to get deleted as well
    +        - --txt-owner-id={{ owner-id-for-this-external-dns }}
    +        - --exoscale-apikey={{ api-key}}
    +        - --exoscale-apisecret={{ api-secret }}
    +        # - --exoscale-apizone={{ api-zone }}
    +        # - --exoscale-apienv={{ api-env }}
    +
    +

    Optional arguments --exoscale-apizone and --exoscale-apienv define Exoscale API Zone
    +(default ch-gva-2) and Exoscale API environment (default api, can be used to target non-production API server) respectively.

    +

    RBAC

    +

    If your cluster is RBAC enabled, you also need to setup the following, before you can run external-dns:

    +
    apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +  namespace: default
    +
    +---
    +
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRole
    +metadata:
    +  name: external-dns
    +rules:
    +- apiGroups: [""]
    +  resources: ["services","endpoints","pods"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: ["extensions","networking.k8s.io"]
    +  resources: ["ingresses"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: [""]
    +  resources: ["nodes"]
    +  verbs: ["list"]
    +
    +---
    +
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRoleBinding
    +metadata:
    +  name: external-dns-viewer
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: ClusterRole
    +  name: external-dns
    +subjects:
    +- kind: ServiceAccount
    +  name: external-dns
    +  namespace: default
    +
    +

    Testing and Verification

    +

    Important!: Remember to change example.com with your own domain throughout the following text.

    +

    Spin up a simple nginx HTTP server with the following spec (kubectl apply -f):

    +
    apiVersion: networking.k8s.io/v1
    +kind: Ingress
    +metadata:
    +  name: nginx
    +  annotations:
    +    external-dns.alpha.kubernetes.io/target: {{ Elastic-IP-address }}
    +spec:
    +  ingressClassName: nginx
    +  rules:
    +  - host: via-ingress.example.com
    +    http:
    +      paths:
    +      - backend:
    +          service:
    +            name: "nginx"
    +            port:
    +              number: 80
    +        path: /
    +        pathType: Prefix
    +
    +---
    +
    +apiVersion: v1
    +kind: Service
    +metadata:
    +  name: nginx
    +spec:
    +  ports:
    +  - port: 80
    +    targetPort: 80
    +  selector:
    +    app: nginx
    +
    +---
    +
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: nginx
    +spec:
    +  selector:
    +    matchLabels:
    +      app: nginx
    +  template:
    +    metadata:
    +      labels:
    +        app: nginx
    +    spec:
    +      containers:
    +      - image: nginx
    +        name: nginx
    +        ports:
    +        - containerPort: 80
    +
    +

    Important!: Don’t run dig, nslookup or similar immediately (until you’ve
    +confirmed the record exists). You’ll get hit by negative DNS caching, which is hard to flush.

    +

    Wait about 30s-1m (interval for external-dns to kick in), then check Exoscales portal… via-ingress.example.com should appear as a A and TXT record with your Elastic-IP-address.

    + +
    +
    + + + Last update: + August 14, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/externalname/index.html b/v0.13.6/tutorials/externalname/index.html new file mode 100644 index 0000000000..96145b0532 --- /dev/null +++ b/v0.13.6/tutorials/externalname/index.html @@ -0,0 +1,2110 @@ + + + + + + + + + + + + + + + + + + Setting up ExternalDNS for ExternalName Services - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + +
    +
    +
    + + + +
    +
    +
    + + +
    +
    + + + + + + + + +

    Setting up ExternalDNS for ExternalName Services

    +

    This tutorial describes how to setup ExternalDNS for usage in conjunction with an ExternalName service.

    +

    Use cases

    +

    The main use cases that inspired this feature is the necessity for having a subdomain pointing to an external domain. In this scenario, it makes sense for the subdomain to have a CNAME record pointing to the external domain.

    +

    Setup

    +

    External DNS

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --log-level=debug
    +        - --source=service
    +        - --source=ingress
    +        - --namespace=dev
    +        - --domain-filter=example.org.
    +        - --provider=aws
    +        - --registry=txt
    +        - --txt-owner-id=dev.example.org
    +
    +

    ExternalName Service

    +
    kind: Service
    +apiVersion: v1
    +metadata:
    +  name: aws-service
    +  annotations:
    +    external-dns.alpha.kubernetes.io/hostname: tenant1.example.org,tenant2.example.org
    +spec:
    +  type: ExternalName
    +  externalName: aws.example.org
    +
    +

    This will create 2 CNAME records pointing to aws.example.org:
    +

    tenant1.example.org
    +tenant2.example.org
    +

    +

    ExternalName Service with an IP address

    +

    If externalName is an IP address, External DNS will create A records instead of CNAME.

    +
    kind: Service
    +apiVersion: v1
    +metadata:
    +  name: aws-service
    +  annotations:
    +    external-dns.alpha.kubernetes.io/hostname: tenant1.example.org,tenant2.example.org
    +spec:
    +  type: ExternalName
    +  externalName: 111.111.111.111
    +
    +

    This will create 2 A records pointing to 111.111.111.111:
    +

    tenant1.example.org
    +tenant2.example.org
    +

    + +
    +
    + + + Last update: + May 29, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/f5-virtualserver/index.html b/v0.13.6/tutorials/f5-virtualserver/index.html new file mode 100644 index 0000000000..20c2ed2939 --- /dev/null +++ b/v0.13.6/tutorials/f5-virtualserver/index.html @@ -0,0 +1,2008 @@ + + + + + + + + + + + + + + + + + + Configuring ExternalDNS to use the F5 Networks VirtualServer Source - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + +
    +
    +
    + + + +
    +
    +
    + + +
    +
    + + + + + + + + +

    Configuring ExternalDNS to use the F5 Networks VirtualServer Source

    +

    This tutorial describes how to configure ExternalDNS to use the F5 Networks VirtualServer Source. It is meant to supplement the other provider-specific setup tutorials.

    +

    The F5 Networks VirtualServer CRD is part of this project. See more in-depth info regarding the VirtualServer CRD here.

    +

    Start with ExternalDNS with the F5 Networks VirtualServer source

    +
      +
    1. +

      Make sure that you have the k8s-bigip-ctlr installed in your cluster. The needed CRDs are bundled within the controller.

      +
    2. +
    3. +

      In your Helm values.yaml add:
      +

      sources:
      +  - ...
      +  - f5-virtualserver
      +  - ...
      +

      +or add it in your Deployment if you aren’t installing external-dns via Helm:
      +
      args:
      +- --source=f5-virtualserver
      +

      +
    4. +
    +

    Note that, in case you’re not installing via Helm, you’ll need the following in the ClusterRole bound to the service account of external-dns:
    +

    - apiGroups:
    +  - cis.f5.com
    +  resources:
    +  - virtualservers
    +  verbs:
    +  - get
    +  - list
    +  - watch
    +

    + +
    +
    + + + Last update: + October 25, 2022 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/gandi/index.html b/v0.13.6/tutorials/gandi/index.html new file mode 100644 index 0000000000..203fc93c52 --- /dev/null +++ b/v0.13.6/tutorials/gandi/index.html @@ -0,0 +1,2243 @@ + + + + + + + + + + + + + + + + + + Setting up ExternalDNS for Services on Gandi - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + + + + +
    +
    + + + + + + + + +

    Setting up ExternalDNS for Services on Gandi

    +

    This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using Gandi.

    +

    Make sure to use >=0.7.7 version of ExternalDNS for this tutorial.

    +

    Creating a Gandi DNS zone (domain)

    +

    Create a new DNS zone where you want to create your records in. Let’s use example.com as an example here. Make sure the zone uses

    +

    Creating Gandi API Key

    +

    Generate an API key on your account (click on “Security”).

    +

    The environment variable GANDI_KEY will be needed to run ExternalDNS with Gandi.

    +

    Deploy ExternalDNS

    +

    Connect your kubectl client to the cluster you want to test ExternalDNS with.
    +Then apply one of the following manifests file to deploy ExternalDNS.

    +

    Manifest (for clusters without RBAC enabled)

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  replicas: 1
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  strategy:
    +    type: Recreate
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service # ingress is also possible
    +        - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
    +        - --provider=gandi
    +        env:
    +        - name: GANDI_KEY
    +          value: "YOUR_GANDI_API_KEY"
    +
    +

    Manifest (for clusters with RBAC enabled)

    +
    apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRole
    +metadata:
    +  name: external-dns
    +rules:
    +- apiGroups: [""]
    +  resources: ["services","endpoints","pods"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: ["extensions","networking.k8s.io"]
    +  resources: ["ingresses"] 
    +  verbs: ["get","watch","list"]
    +- apiGroups: [""]
    +  resources: ["nodes"]
    +  verbs: ["list","watch"]
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRoleBinding
    +metadata:
    +  name: external-dns-viewer
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: ClusterRole
    +  name: external-dns
    +subjects:
    +- kind: ServiceAccount
    +  name: external-dns
    +  namespace: default
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  replicas: 1
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  strategy:
    +    type: Recreate
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      serviceAccountName: external-dns
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service # ingress is also possible
    +        - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
    +        - --provider=gandi
    +        env:
    +        - name: GANDI_KEY
    +          value: "YOUR_GANDI_API_KEY"
    +
    +

    Deploying an Nginx Service

    +

    Create a service file called ‘nginx.yaml’ with the following contents:

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: nginx
    +spec:
    +  replicas: 1
    +  selector:
    +    matchLabels:
    +      app: nginx
    +  template:
    +    metadata:
    +      labels:
    +        app: nginx
    +    spec:
    +      containers:
    +      - image: nginx
    +        name: nginx
    +        ports:
    +        - containerPort: 80
    +---
    +apiVersion: v1
    +kind: Service
    +metadata:
    +  name: nginx
    +  annotations:
    +    external-dns.alpha.kubernetes.io/hostname: my-app.example.com
    +spec:
    +  selector:
    +    app: nginx
    +  type: LoadBalancer
    +  ports:
    +    - protocol: TCP
    +      port: 80
    +      targetPort: 80
    +
    +

    Note the annotation on the service; use the same hostname as the Gandi Domain. Make sure that your Domain is configured to use Live-DNS.

    +

    ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records.

    +

    Create the deployment and service:

    +
    $ kubectl create -f nginx.yaml
    +
    +

    Depending where you run your service it can take a little while for your cloud provider to create an external IP for the service.

    +

    Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize the Gandi DNS records.

    +

    Verifying Gandi DNS records

    +

    Check your Gandi Dashboard to view the records for your Gandi DNS zone.

    +

    Click on the zone for the one created above if a different domain was used.

    +

    This should show the external IP address of the service as the A record for your domain.

    +

    Cleanup

    +

    Now that we have verified that ExternalDNS will automatically manage Gandi DNS records, we can delete the tutorial’s example:

    +
    $ kubectl delete service -f nginx.yaml
    +$ kubectl delete service -f externaldns.yaml
    +
    +

    Additional options

    +

    If you’re using organizations to separate your domains, you can pass the organization’s ID in an environment variable called GANDI_SHARING_ID to get access to it.

    + +
    +
    + + + Last update: + May 31, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/gateway-api/index.html b/v0.13.6/tutorials/gateway-api/index.html new file mode 100644 index 0000000000..c71c3a5561 --- /dev/null +++ b/v0.13.6/tutorials/gateway-api/index.html @@ -0,0 +1,2094 @@ + + + + + + + + + + + + + + + + + + Configuring ExternalDNS to use Gateway API Route Sources - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + +
    +
    +
    + + + +
    +
    +
    + + +
    +
    + + + + + + + + +

    Configuring ExternalDNS to use Gateway API Route Sources

    +

    This describes how to configure ExternalDNS to use Gateway API Route sources.
    +It is meant to supplement the other provider-specific setup tutorials.

    +

    Supported API Versions

    +

    As the Gateway API is still in an experimental phase, ExternalDNS makes no backwards
    +compatibilty guarantees regarding its support. However, it currently supports a mixture of
    +v1alpha2 and v1beta1 APIs. Gateways and HTTPRoutes are supported using the v1beta1 API.
    +GRPCRoutes, TLSRoutes, TCPRoutes, and UDPRoutes are supported using the v1alpha2 API.

    +

    Hostnames

    +

    HTTPRoute and TLSRoute specs, along with their associated Gateway Listeners, contain hostnames that
    +will be used by ExternalDNS. However, no such hostnames may be specified in TCPRoute or UDPRoute
    +specs. For TCPRoutes and UDPRoutes, the external-dns.alpha.kubernetes.io/hostname annotation
    +is the recommended way to provide their hostnames to ExternalDNS. This annotation is also supported
    +for HTTPRoutes and TLSRoutes by ExternalDNS, but it’s strongly recommended that they use their
    +specs to provide all intended hostnames, since the Gateway that ultimately routes their
    +requests/connections won’t recognize additional hostnames from the annotation.

    +

    Manifest with RBAC

    +
    apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +  namespace: default
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRole
    +metadata:
    +  name: external-dns
    +rules:
    +- apiGroups: [""]
    +  resources: ["namespaces"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: ["gateway.networking.k8s.io"]
    +  resources: ["gateways","httproutes","grpcroutes","tlsroutes","tcproutes","udproutes"] 
    +  verbs: ["get","watch","list"]
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRoleBinding
    +metadata:
    +  name: external-dns
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: ClusterRole
    +  name: external-dns
    +subjects:
    +- kind: ServiceAccount
    +  name: external-dns
    +  namespace: default
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +  namespace: default
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      serviceAccountName: external-dns
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        # Add desired Gateway API Route sources.
    +        - --source=gateway-httproute
    +        - --source=gateway-grpcroute
    +        - --source=gateway-tlsroute
    +        - --source=gateway-tcproute
    +        - --source=gateway-udproute
    +        # Optionally, limit Routes to those in the given namespace.
    +        - --namespace=my-route-namespace
    +        # Optionally, limit Routes to those matching the given label selector.
    +        - --label-filter=my-route-label==my-route-value
    +        # Optionally, limit Route endpoints to those Gateways in the given namespace.
    +        - --gateway-namespace=my-gateway-namespace
    +        # Optionally, limit Route endpoints to those Gateways matching the given label selector.
    +        - --gateway-label-filter=my-gateway-label==my-gateway-value
    +        # Add provider-specific flags...
    +        - --domain-filter=external-dns-test.my-org.com
    +        - --provider=google
    +        - --registry=txt
    +        - --txt-owner-id=my-identifier
    +
    + +
    +
    + + + Last update: + May 29, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/gke/index.html b/v0.13.6/tutorials/gke/index.html new file mode 100644 index 0000000000..7917619968 --- /dev/null +++ b/v0.13.6/tutorials/gke/index.html @@ -0,0 +1,2737 @@ + + + + + + + + + + + + + + + + + + Setting up ExternalDNS on Google Kubernetes Engine - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + + + + +
    +
    + + + + + + + + +

    Setting up ExternalDNS on Google Kubernetes Engine

    +

    This tutorial describes how to setup ExternalDNS for usage within a GKE (Google Kuberentes Engine) cluster. Make sure to use >=0.11.0 version of ExternalDNS for this tutorial

    +

    Single project test scenario using access scopes

    +

    If you prefer to try-out ExternalDNS in one of the existing environments you can skip this step

    +

    The following instructions use access scopes to provide ExternalDNS with the permissions it needs to manage DNS records within a single project, the organizing entity to allocate resources.

    +

    Note that since these permissions are associated with the instance, all pods in the cluster will also have these permissions. As such, this approach is not suitable for anything but testing environments.

    +

    This solution will only work when both CloudDNS and GKE are provisioned in the same project. If the CloudDNS zone is in a different project, this solution will not work.

    +

    Configure Project Environment

    +

    Setup your environment to work with Google Cloud Platform. Fill in your variables as needed, e.g. target project.

    +
    # set variables to the appropriate desired values
    +PROJECT_ID="my-external-dns-test"
    +REGION="europe-west1"
    +ZONE="europe-west1-d"
    +ClOUD_BILLING_ACCOUNT="<my-cloud-billing-account>"
    +# set default settings for project
    +gcloud config set project $PROJECT_ID
    +gcloud config set compute/region $REGION
    +gcloud config set compute/zone $ZONE
    +# enable billing and APIs if not done already
    +gcloud beta billing projects link $PROJECT_ID \
    +  --billing-account $BILLING_ACCOUNT
    +gcloud services enable "dns.googleapis.com"
    +gcloud services enable "container.googleapis.com"
    +
    +

    Create GKE Cluster

    +
    gcloud container clusters create $GKE_CLUSTER_NAME \
    +  --num-nodes 1 \
    +  --scopes "https://www.googleapis.com/auth/ndev.clouddns.readwrite"
    +
    +

    WARNING: Note that this cluster will use the default compute engine GSA that contians the overly permissive project editor (roles/editor) role. So essentially, anything on the cluster could potentially grant escalated privileges. Also, as mentioned earlier, the access scope ndev.clouddns.readwrite will allow anything running on the cluster to have read/write permissions on all Cloud DNS zones within the same project.

    +

    Cloud DNS Zone

    +

    Create a DNS zone which will contain the managed DNS records. If using your own domain that was registered with a third-party domain registrar, you should point your domain’s name servers to the values under the nameServers key. Please consult your registrar’s documentation on how to do that. This tutorial will use example domain of example.com.

    +
    gcloud dns managed-zones create "example-com" --dns-name "example.com." \
    +  --description "Automatically managed zone by kubernetes.io/external-dns"
    +
    +

    Make a note of the nameservers that were assigned to your new zone.

    +
    gcloud dns record-sets list \
    +    --zone "example-com" --name "example.com." --type NS
    +
    +

    Outputs:

    +
    NAME          TYPE  TTL    DATA
    +example.com.  NS    21600  ns-cloud-e1.googledomains.com.,ns-cloud-e2.googledomains.com.,ns-cloud-e3.googledomains.com.,ns-cloud-e4.googledomains.com.
    +
    +

    In this case it’s ns-cloud-{e1-e4}.googledomains.com. but your’s could slightly differ, e.g. {a1-a4}, {b1-b4} etc.

    +

    Cross project access scenario using Google Service Account

    +

    More often, following best practices in regards to security and operations, Cloud DNS zones will be managed in a separate project from the Kubernetes cluster. This section shows how setup ExternalDNS to access Cloud DNS from a different project. These steps will also work for single project scenarios as well.

    +

    ExternalDNS will need permissions to make changes to the Cloud DNS zone. There are three ways to configure the access needed:

    + +

    Setup Cloud DNS and GKE

    +

    Below are examples on how you can configure Cloud DNS and GKE in separate projects, and then use one of the three methods to grant access to ExternalDNS. Replace the environment variables to values that make sense in your environment.

    +

    Configure Projects

    +

    For this process, create projects with the appropriate APIs enabled.

    +
    # set variables to appropriate desired values
    +GKE_PROJECT_ID="my-workload-project"
    +DNS_PROJECT_ID="my-cloud-dns-project"
    +ClOUD_BILLING_ACCOUNT="<my-cloud-billing-account>"
    +# enable billing and APIs for DNS project if not done already
    +gcloud config set project $DNS_PROJECT_ID
    +gcloud beta billing projects link $CLOUD_DNS_PROJECT \
    +  --billing-account $ClOUD_BILLING_ACCOUNT
    +gcloud services enable "dns.googleapis.com"
    +# enable billing and APIs for GKE project if not done already
    +gcloud config set project $GKE_PROJECT_ID
    +gcloud beta billing projects link $CLOUD_DNS_PROJECT \
    +  --billing-account $ClOUD_BILLING_ACCOUNT
    +gcloud services enable "container.googleapis.com"
    +
    +

    Provisioning Cloud DNS

    +

    Create a Cloud DNS zone in the designated DNS project.

    +
    gcloud dns managed-zones create "example-com" --project $DNS_PROJECT_ID \
    +  --description "example.com" --dns-name="example.com." --visibility=public
    +
    +

    If using your own domain that was registered with a third-party domain registrar, you should point your domain’s name servers to the values under the nameServers key. Please consult your registrar’s documentation on how to do that. The example domain of example.com will be used for this tutorial.

    +

    Provisioning a GKE cluster for cross project access

    +

    Create a GSA (Google Service Account) and grant it the minimal set of privileges required for GKE nodes:

    +
    GKE_CLUSTER_NAME="my-external-dns-cluster"
    +GKE_REGION="us-central1"
    +GKE_SA_NAME="worker-nodes-sa"
    +GKE_SA_EMAIL="$GKE_SA_NAME@${GKE_PROJECT_ID}.iam.gserviceaccount.com"
    +
    +ROLES=(
    +  roles/logging.logWriter
    +  roles/monitoring.metricWriter
    +  roles/monitoring.viewer
    +  roles/stackdriver.resourceMetadata.writer
    +)
    +
    +gcloud iam service-accounts create $GKE_SA_NAME \
    +  --display-name $GKE_SA_NAME --project $GKE_PROJECT_ID
    +
    +# assign google service account to roles in GKE project
    +for ROLE in ${ROLES[*]}; do
    +  gcloud projects add-iam-policy-binding $GKE_PROJECT_ID \
    +    --member "serviceAccount:$GKE_SA_EMAIL" \
    +    --role $ROLE
    +done
    +
    +

    Create a cluster using this service account and enable workload identity:

    +
    gcloud container clusters create $GKE_CLUSTER_NAME \
    +  --project $GKE_PROJECT_ID --region $GKE_REGION --num-nodes 1 \
    +  --service-account "$GKE_SA_EMAIL" \
    +  --workload-pool "$GKE_PROJECT_ID.svc.id.goog"
    +
    +

    Worker Node Service Account method

    +

    In this method, the GSA (Google Service Account) that is associated with GKE worker nodes will be configured to have access to Cloud DNS.

    +

    WARNING: This will grant access to modify the Cloud DNS zone records for all containers running on cluster, not just ExternalDNS, so use this option with caution. This is not recommended for production environments.

    +
    GKE_SA_EMAIL="$GKE_SA_NAME@${GKE_PROJECT_ID}.iam.gserviceaccount.com"
    +
    +# assign google service account to dns.admin role in the cloud dns project
    +gcloud projects add-iam-policy-binding $DNS_PROJECT_ID \
    +  --member serviceAccount:$GKE_SA_EMAIL \
    +  --role roles/dns.admin
    +
    +

    After this, follow the steps in Deploy ExternalDNS. Make sure to set the --google-project flag to match the Cloud DNS project name.

    +

    Static Credentials

    +

    In this scenario, a new GSA (Google Service Account) is created that has access to the CloudDNS zone. The credentials for this GSA are saved and installed as a Kubernetes secret that will be used by ExternalDNS.

    +

    This allows only containers that have access to the secret, such as ExternalDNS to update records on the Cloud DNS Zone.

    +

    Create GSA for use with static credentials

    +
    DNS_SA_NAME="external-dns-sa"
    +DNS_SA_EMAIL="$DNS_SA_NAME@${GKE_PROJECT_ID}.iam.gserviceaccount.com"
    +
    +# create GSA used to access the Cloud DNS zone
    +gcloud iam service-accounts create $DNS_SA_NAME --display-name $DNS_SA_NAME
    +
    +# assign google service account to dns.admin role in cloud-dns project
    +gcloud projects add-iam-policy-binding $DNS_PROJECT_ID \
    +  --member serviceAccount:$DNS_SA_EMAIL --role "roles/dns.admin"
    +
    +

    Create Kubernetes secret using static credentials

    +

    Generate static credentials from the ExternalDNS GSA.

    +
    # download static credentials
    +gcloud iam service-accounts keys create /local/path/to/credentials.json \
    +  --iam-account $DNS_SA_EMAIL
    +
    +

    Create a Kubernetes secret with the credentials in the same namespace of ExternalDNS.

    +
    kubectl create secret generic "external-dns" --namespace ${EXTERNALDNS_NS:-"default"} \
    +  --from-file /local/path/to/credentials.json
    +
    +

    After this, follow the steps in Deploy ExternalDNS. Make sure to set the --google-project flag to match Cloud DNS project name. Make sure to uncomment out the section that mounts the secret to the ExternalDNS pods.

    +

    Workload Identity

    +

    Workload Identity allows workloads in your GKE cluster to impersonate GSA (Google Service Accounts) using KSA (Kubernetes Service Accounts) configured during deployemnt. These are the steps to use this feature with ExternalDNS.

    +

    Create GSA for use with Workload Identity

    +
    DNS_SA_NAME="external-dns-sa"
    +DNS_SA_EMAIL="$DNS_SA_NAME@${GKE_PROJECT_ID}.iam.gserviceaccount.com"
    +
    +gcloud iam service-accounts create $DNS_SA_NAME --display-name $DNS_SA_NAME
    +gcloud projects add-iam-policy-binding $DNS_PROJECT_ID \
    +   --member serviceAccount:$DNS_SA_EMAIL --role "roles/dns.admin"
    +
    + +

    Add an IAM policy binding bewtween the workload identity GSA and ExternalDNS GSA. This will link the ExternalDNS KSA to ExternalDNS GSA.

    +
    gcloud iam service-accounts add-iam-policy-binding $DNS_SA_EMAIL \
    +  --role "roles/iam.workloadIdentityUser" \
    +  --member "serviceAccount:$GKE_PROJECT_ID.svc.id.goog[${EXTERNALDNS_NS:-"default"}/external-dns]"
    +
    +

    Deploy External DNS

    +

    Deploy ExternalDNS with the following steps below, documented under Deploy ExternalDNS. Set the --google-project flag to the Cloud DNS project name.

    + +

    Add the proper workload identity annotation to the ExternalDNS KSA.

    +
    kubectl annotate serviceaccount "external-dns" \
    +  --namespace ${EXTERNALDNS_NS:-"default"} \
    +  "iam.gke.io/gcp-service-account=$DNS_SA_EMAIL"
    +
    +

    Update ExternalDNS pods

    +

    Update the Pod spec to schedule the workloads on nodes that use Workload Identity and to use the annotated Kubernetes service account.

    +
    kubectl patch deployment "external-dns" \
    +  --namespace ${EXTERNALDNS_NS:-"default"} \
    +  --patch \
    + '{"spec": {"template": {"spec": {"nodeSelector": {"iam.gke.io/gke-metadata-server-enabled": "true"}}}}}'
    +
    +

    After all of these steps you may see several messages with googleapi: Error 403: Forbidden, forbidden. After several minutes when the token is refreshed, these error messages will go away, and you should see info messages, such as: All records are already up to date.

    +

    Deploy ExternalDNS

    +

    Then apply the following manifests file to deploy ExternalDNS.

    +
    apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +  labels:
    +    app.kubernetes.io/name: external-dns
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRole
    +metadata:
    +  name: external-dns
    +  labels:
    +    app.kubernetes.io/name: external-dns
    +rules:
    +  - apiGroups: [""]
    +    resources: ["services","endpoints","pods","nodes"]
    +    verbs: ["get","watch","list"]
    +  - apiGroups: ["extensions","networking.k8s.io"]
    +    resources: ["ingresses"]
    +    verbs: ["get","watch","list"]
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRoleBinding
    +metadata:
    +  name: external-dns-viewer
    +  labels:
    +    app.kubernetes.io/name: external-dns
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: ClusterRole
    +  name: external-dns
    +subjects:
    +  - kind: ServiceAccount
    +    name: external-dns
    +    namespace: default # change if namespace is not 'default'
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +  labels:
    +    app.kubernetes.io/name: external-dns  
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app.kubernetes.io/name: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app.kubernetes.io/name: external-dns
    +    spec:
    +      serviceAccountName: external-dns
    +      containers:
    +        - name: external-dns
    +          image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +          args:
    +            - --source=service
    +            - --source=ingress
    +            - --domain-filter=example.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones
    +            - --provider=google
    +            - --log-format=json # google cloud logs parses severity of the "text" log format incorrectly
    +    #        - --google-project=my-cloud-dns-project # Use this to specify a project different from the one external-dns is running inside
    +            - --google-zone-visibility=public # Use this to filter to only zones with this visibility. Set to either 'public' or 'private'. Omitting will match public and private zones
    +            - --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization
    +            - --registry=txt
    +            - --txt-owner-id=my-identifier
    +      #     # uncomment below if static credentials are used  
    +      #     env:
    +      #       - name: GOOGLE_APPLICATION_CREDENTIALS
    +      #         value: /etc/secrets/service-account/credentials.json
    +      #     volumeMounts:
    +      #       - name: google-service-account
    +      #         mountPath: /etc/secrets/service-account/
    +      # volumes:
    +      #   - name: google-service-account
    +      #     secret:
    +      #       secretName: external-dns
    +
    +

    Create the deployment for ExternalDNS:

    +
    kubectl create --namespace "default" --filename externaldns.yaml
    +
    +

    Verify ExternalDNS works

    +

    The following will deploy a small nginx server that will be used to demonstrate that ExternalDNS is working.

    +

    Verify using an external load balancer

    +

    Create the following sample application to test that ExternalDNS works. This example will provision a L4 load balancer.

    +
    apiVersion: v1
    +kind: Service
    +metadata:
    +  name: nginx
    +  annotations:
    +    # change nginx.example.com to match an appropriate value
    +    external-dns.alpha.kubernetes.io/hostname: nginx.example.com
    +spec:
    +  type: LoadBalancer
    +  ports:
    +    - port: 80
    +      targetPort: 80
    +  selector:
    +    app: nginx
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: nginx
    +spec:
    +  selector:
    +    matchLabels:
    +      app: nginx
    +  template:
    +    metadata:
    +      labels:
    +        app: nginx
    +    spec:
    +      containers:
    +        - image: nginx
    +          name: nginx
    +          ports:
    +            - containerPort: 80
    +
    +

    Create the deployment and service objects:

    +
    kubectl create --namespace "default" --filename nginx.yaml
    +
    +

    After roughly two minutes check that a corresponding DNS record for your service was created.

    +
    gcloud dns record-sets list --zone "example-com" --name "nginx.example.com."
    +
    +

    Example output:

    +
    NAME                TYPE  TTL  DATA
    +nginx.example.com.  A     300  104.155.60.49
    +nginx.example.com.  TXT   300  "heritage=external-dns,external-dns/owner=my-identifier"
    +
    +

    Note created TXT record alongside A record. TXT record signifies that the corresponding A record is managed by ExternalDNS. This makes ExternalDNS safe for running in environments where there are other records managed via other means.

    +

    Let’s check that we can resolve this DNS name. We’ll ask the nameservers assigned to your zone first.

    +
    dig +short @ns-cloud-e1.googledomains.com. nginx.example.com.
    +104.155.60.49
    +
    +

    Given you hooked up your DNS zone with its parent zone you can use curl to access your site.

    +
    curl nginx.example.com
    +
    +

    Verify using an ingress

    +

    Let’s check that Ingress works as well. Create the following Ingress.

    +
    apiVersion: networking.k8s.io/v1
    +kind: Ingress
    +metadata:
    +  name: nginx
    +spec:
    +  rules:
    +    - host: server.example.com
    +      http:
    +        paths:
    +          - path: /
    +            pathType: Prefix
    +            backend:
    +              service:
    +                name: nginx
    +                port:
    +                  number: 80
    +
    +

    Create the ingress objects with:

    +
    kubectl create --namespace "default" --filename ingress.yaml
    +
    +

    Note that this will ingress object will use the default ingress controller that comes with GKE to create a L7 load balancer in addition to the L4 load balancer previously with the service object. To use only the L7 load balancer, update the service manafest to change the Service type to NodePort and remove the ExternalDNS annotation.

    +

    After roughly two minutes check that a corresponding DNS record for your Ingress was created.

    +

    gcloud dns record-sets list \
    +    --zone "example-com" \
    +    --name "server.example.com." \
    +

    +Output:

    +
    NAME                 TYPE  TTL  DATA
    +server.example.com.  A     300  130.211.46.224
    +server.example.com.  TXT   300  "heritage=external-dns,external-dns/owner=my-identifier"
    +
    +

    Let’s check that we can resolve this DNS name as well.

    +
    dig +short @ns-cloud-e1.googledomains.com. server.example.com.
    +130.211.46.224
    +
    +

    Try with curl as well.

    +
    curl server.example.com
    +
    +

    Clean up

    +

    Make sure to delete all Service and Ingress objects before terminating the cluster so all load balancers get cleaned up correctly.

    +
    kubectl delete service nginx
    +kubectl delete ingress nginx
    +
    +

    Give ExternalDNS some time to clean up the DNS records for you. Then delete the managed zone and cluster.

    +
    gcloud dns managed-zones delete "example-com"
    +gcloud container clusters delete "external-dns"
    +
    + +
    +
    + + + Last update: + May 29, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/gloo-proxy/index.html b/v0.13.6/tutorials/gloo-proxy/index.html new file mode 100644 index 0000000000..80ac5f38b9 --- /dev/null +++ b/v0.13.6/tutorials/gloo-proxy/index.html @@ -0,0 +1,2086 @@ + + + + + + + + + + + + + + + + + + Configuring ExternalDNS to use the Gloo Proxy Source - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + +
    +
    +
    + + + +
    +
    +
    + + +
    +
    + + + + + + + + +

    Configuring ExternalDNS to use the Gloo Proxy Source

    +

    This tutorial describes how to configure ExternalDNS to use the Gloo Proxy source.
    +It is meant to supplement the other provider-specific setup tutorials.

    +

    Manifest (for clusters without RBAC enabled)

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      containers:
    +      - name: external-dns
    +        # update this to the desired external-dns version
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=gloo-proxy
    +        - --gloo-namespace=custom-gloo-system # gloo system namespace. Omit to use the default (gloo-system)
    +        - --provider=aws
    +        - --registry=txt
    +        - --txt-owner-id=my-identifier
    +
    +

    Manifest (for clusters with RBAC enabled)

    +

    Could be change if you have mulitple sources

    +
    apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRole
    +metadata:
    +  name: external-dns
    +rules:
    +- apiGroups: [""]
    +  resources: ["services","endpoints","pods"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: [""]
    +  resources: ["nodes"]
    +  verbs: ["list","watch"]
    +- apiGroups: ["gloo.solo.io"]
    +  resources: ["proxies"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: ["gateway.solo.io"]
    +  resources: ["virtualservices"]
    +  verbs: ["get", "list", "watch"]
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRoleBinding
    +metadata:
    +  name: external-dns-viewer
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: ClusterRole
    +  name: external-dns
    +subjects:
    +- kind: ServiceAccount
    +  name: external-dns
    +  namespace: default
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      serviceAccountName: external-dns
    +      containers:
    +      - name: external-dns
    +        # update this to the desired external-dns version
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=gloo-proxy
    +        - --gloo-namespace=custom-gloo-system # gloo system namespace. Omit to use the default (gloo-system)
    +        - --provider=aws
    +        - --registry=txt
    +        - --txt-owner-id=my-identifier
    +
    + +
    +
    + + + Last update: + May 29, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/godaddy/index.html b/v0.13.6/tutorials/godaddy/index.html new file mode 100644 index 0000000000..4702b29864 --- /dev/null +++ b/v0.13.6/tutorials/godaddy/index.html @@ -0,0 +1,2275 @@ + + + + + + + + + + + + + + + + + + Setting up ExternalDNS for Services on GoDaddy - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + + + + +
    +
    + + + + + + + + +

    Setting up ExternalDNS for Services on GoDaddy

    +

    This tutorial describes how to setup ExternalDNS for use within a
    +Kubernetes cluster using GoDaddy DNS.

    +

    Make sure to use >=0.6 version of ExternalDNS for this tutorial.

    +

    Creating a zone with GoDaddy DNS

    +

    If you are new to GoDaddy, we recommend you first read the following
    +instructions for creating a zone.

    +

    Creating a zone using the GoDaddy web console

    +

    Creating a zone using the GoDaddy API

    +

    Creating GoDaddy API key

    +

    You first need to create an API Key.

    +

    Using the GoDaddy documentation you will have your API key and API secret

    +

    Deploy ExternalDNS

    +

    Connect your kubectl client to the cluster with which you want to test ExternalDNS, and then apply one of the following manifest files for deployment:

    +

    Manifest (for clusters without RBAC enabled)

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service # ingress is also possible
    +        - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
    +        - --provider=godaddy
    +        - --txt-prefix=external-dns. # In case of multiple k8s cluster
    +        - --txt-owner-id=owner-id # In case of multiple k8s cluster
    +        - --godaddy-api-key=<Your API Key>
    +        - --godaddy-api-secret=<Your API secret>
    +
    +

    Manifest (for clusters with RBAC enabled)

    +
    apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRole
    +metadata:
    +  name: external-dns
    +rules:
    +- apiGroups: [""]
    +  resources: ["services"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: [""]
    +  resources: ["pods"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: ["extensions","networking.k8s.io"]
    +  resources: ["ingresses"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: [""]
    +  resources: ["nodes"]
    +  verbs: ["list","watch"]
    +- apiGroups: [""]
    +  resources: ["endpoints"]
    +  verbs: ["get","watch","list"]
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRoleBinding
    +metadata:
    +  name: external-dns-viewer
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: ClusterRole
    +  name: external-dns
    +subjects:
    +- kind: ServiceAccount
    +  name: external-dns
    +  namespace: default
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      serviceAccountName: external-dns
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service # ingress is also possible
    +        - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
    +        - --provider=godaddy
    +        - --txt-prefix=external-dns. # In case of multiple k8s cluster
    +        - --txt-owner-id=owner-id # In case of multiple k8s cluster
    +        - --godaddy-api-key=<Your API Key>
    +        - --godaddy-api-secret=<Your API secret>
    +
    +

    Deploying an Nginx Service

    +

    Create a service file called ‘nginx.yaml’ with the following contents:

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: nginx
    +spec:
    +  selector:
    +    matchLabels:
    +      app: nginx
    +  template:
    +    metadata:
    +      labels:
    +        app: nginx
    +    spec:
    +      containers:
    +      - image: nginx
    +        name: nginx
    +        ports:
    +        - containerPort: 80
    +---
    +apiVersion: v1
    +kind: Service
    +metadata:
    +  name: nginx
    +  annotations:
    +    external-dns.alpha.kubernetes.io/hostname: example.com
    +    external-dns.alpha.kubernetes.io/ttl: "120" #optional
    +spec:
    +  selector:
    +    app: nginx
    +  type: LoadBalancer
    +  ports:
    +    - protocol: TCP
    +      port: 80
    +      targetPort: 80
    +
    +

    A note about annotations

    +

    Verify that the annotation on the service uses the same hostname as the GoDaddy DNS zone created above. The annotation may also be a subdomain of the DNS zone (e.g. ‘www.example.com’).

    +

    The TTL annotation can be used to configure the TTL on DNS records managed by ExternalDNS and is optional. If this annotation is not set, the TTL on records managed by ExternalDNS will default to 10.

    +

    ExternalDNS uses the hostname annotation to determine which services should be registered with DNS. Removing the hostname annotation will cause ExternalDNS to remove the corresponding DNS records.

    +

    Create the deployment and service

    +
    $ kubectl create -f nginx.yaml
    +
    +

    Depending on where you run your service, it may take some time for your cloud provider to create an external IP for the service. Once an external IP is assigned, ExternalDNS detects the new service IP address and synchronizes the GoDaddy DNS records.

    +

    Verifying GoDaddy DNS records

    +

    Use the GoDaddy web console or API to verify that the A record for your domain shows the external IP address of the services.

    +

    Cleanup

    +

    Once you successfully configure and verify record management via ExternalDNS, you can delete the tutorial’s example:

    +
    $ kubectl delete -f nginx.yaml
    +$ kubectl delete -f externaldns.yaml
    +
    + +
    +
    + + + Last update: + May 29, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/hostport/index.html b/v0.13.6/tutorials/hostport/index.html new file mode 100644 index 0000000000..06b5d43489 --- /dev/null +++ b/v0.13.6/tutorials/hostport/index.html @@ -0,0 +1,2320 @@ + + + + + + + + + + + + + + + + + + Setting up ExternalDNS for Headless Services - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + + + + +
    +
    + + + + + + + + +

    Setting up ExternalDNS for Headless Services

    +

    This tutorial describes how to setup ExternalDNS for usage in conjunction with a Headless service.

    +

    Use cases

    +

    The main use cases that inspired this feature is the necessity for fixed addressable hostnames with services, such as Kafka when trying to access them from outside the cluster. In this scenario, quite often, only the Node IP addresses are actually routable and as in systems like Kafka more direct connections are preferable.

    +

    Setup

    +

    We will go through a small example of deploying a simple Kafka with use of a headless service.

    +

    External DNS

    +

    A simple deploy could look like this:

    +

    Manifest (for clusters without RBAC enabled)

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --log-level=debug
    +        - --source=service
    +        - --source=ingress
    +        - --namespace=dev
    +        - --domain-filter=example.org. 
    +        - --provider=aws
    +        - --registry=txt
    +        - --txt-owner-id=dev.example.org
    +
    +

    Manifest (for clusters with RBAC enabled)

    +
    apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRole
    +metadata:
    +  name: external-dns
    +rules:
    +- apiGroups: [""]
    +  resources: ["services","endpoints","pods"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: ["extensions","networking.k8s.io"]
    +  resources: ["ingresses"] 
    +  verbs: ["get","watch","list"]
    +- apiGroups: [""]
    +  resources: ["nodes"]
    +  verbs: ["list"]
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRoleBinding
    +metadata:
    +  name: external-dns-viewer
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: ClusterRole
    +  name: external-dns
    +subjects:
    +- kind: ServiceAccount
    +  name: external-dns
    +  namespace: default
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      serviceAccountName: external-dns
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --log-level=debug
    +        - --source=service
    +        - --source=ingress
    +        - --namespace=dev
    +        - --domain-filter=example.org. 
    +        - --provider=aws
    +        - --registry=txt
    +        - --txt-owner-id=dev.example.org
    +
    +

    Kafka Stateful Set

    +

    First lets deploy a Kafka Stateful set, a simple example(a lot of stuff is missing) with a headless service called ksvc

    +

    apiVersion: apps/v1
    +kind: StatefulSet
    +metadata:
    +  name: kafka
    +spec:
    +  serviceName: ksvc
    +  replicas: 3
    +  template:
    +    metadata:
    +      labels:
    +        component: kafka
    +    spec:
    +      containers:
    +      - name:  kafka        
    +        image: confluent/kafka
    +        ports:
    +        - containerPort: 9092
    +          hostPort: 9092
    +          name: external
    +        command:
    +        - bash
    +        - -c
    +        - " export DOMAIN=$(hostname -d) && \
    +            export KAFKA_BROKER_ID=$(echo $HOSTNAME|rev|cut -d '-' -f 1|rev) && \
    +            export KAFKA_ZOOKEEPER_CONNECT=$ZK_CSVC_SERVICE_HOST:$ZK_CSVC_SERVICE_PORT && \
    +            export KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://$HOSTNAME.example.org:9092 && \
    +            /etc/confluent/docker/run"
    +        volumeMounts:
    +        - name: datadir
    +          mountPath: /var/lib/kafka
    +  volumeClaimTemplates:
    +  - metadata:
    +      name: datadir
    +      annotations:
    +          volume.beta.kubernetes.io/storage-class: st1
    +    spec:
    +      accessModes: [ "ReadWriteOnce" ]
    +      resources:
    +        requests:
    +          storage:  500Gi
    +

    +Very important here, is to set the hostPort(only works if the PodSecurityPolicy allows it)! and in case your app requires an actual hostname inside the container, unlike Kafka, which can advertise on another address, you have to set the hostname yourself.

    +

    Headless Service

    +

    Now we need to define a headless service to use to expose the Kafka pods. There are generally two approaches to use expose the nodeport of a Headless service:

    +
      +
    1. Add --fqdn-template={{name}}.example.org
    2. +
    3. Use a full annotation
    4. +
    +

    If you go with #1, you just need to define the headless service, here is an example of the case #2:

    +

    apiVersion: v1
    +kind: Service
    +metadata:
    +  name: ksvc
    +  annotations:
    +    external-dns.alpha.kubernetes.io/hostname:  example.org
    +spec:
    +  ports:
    +  - port: 9092
    +    name: external
    +  clusterIP: None
    +  selector:
    +    component: kafka
    +

    +This will create 3 dns records:
    +
    kafka-0.example.org
    +kafka-1.example.org
    +kafka-2.example.org
    +

    +

    If you set --fqdn-template={{name}}.example.org you can omit the annotation.
    +Generally it is a better approach to use --fqdn-template={{name}}.example.org, because then
    +you would get the service name inside the generated A records:

    +
    kafka-0.ksvc.example.org
    +kafka-1.ksvc.example.org
    +kafka-2.ksvc.example.org
    +
    +

    Using pods’ HostIPs as targets

    +

    Add the following annotation to your Service:

    +
    external-dns.alpha.kubernetes.io/endpoints-type: HostIP
    +
    +

    external-dns will now publish the value of the .status.hostIP field of the pods backing your Service.

    +

    Using node external IPs as targets

    +

    Add the following annotation to your Service:

    +
    external-dns.alpha.kubernetes.io/endpoints-type: NodeExternalIP
    +
    +

    external-dns will now publish the node external IP (.status.addresses entries of with type: NodeExternalIP) of the nodes on which the pods backing your Service are running.

    +

    Using pod annotations to specify target IPs

    +

    Add the following annotation to the pods backing your Service:

    +
    external-dns.alpha.kubernetes.io/target: "1.2.3.4"
    +
    +

    external-dns will publish the IP specified in the annotation of each pod instead of using the podIP advertised by Kubernetes.

    +

    This can be useful e.g. if you are NATing public IPs onto your pod IPs and want to publish these in DNS.

    + +
    +
    + + + Last update: + May 31, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/ibmcloud/index.html b/v0.13.6/tutorials/ibmcloud/index.html new file mode 100644 index 0000000000..bf5238ea50 --- /dev/null +++ b/v0.13.6/tutorials/ibmcloud/index.html @@ -0,0 +1,2409 @@ + + + + + + + + + + + + + + + + + + Setting up ExternalDNS for Services on IBMCloud - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + + + + +
    +
    + + + + + + + + +

    Setting up ExternalDNS for Services on IBMCloud

    +

    This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using IBMCloud DNS.

    +

    This tutorial uses IBMCloud CLI for all
    +IBM Cloud commands and assumes that the Kubernetes cluster was created via IBM Cloud Kubernetes Service and kubectl commands
    +are being run on an orchestration node.

    +

    Creating a IBMCloud DNS zone

    +

    The IBMCloud provider for ExternalDNS will find suitable zones for domains it manages; it will
    +not automatically create zones.
    +For public zone, This tutorial assume that the IBMCloud Internet Services was provisioned and the cis cli plugin was installed with IBMCloud CLI
    +For private zone, This tutorial assume that the IBMCloud DNS Services was provisioned and the dns cli plugin was installed with IBMCloud CLI

    +

    Public Zone

    +

    For this tutorial, we create public zone named example.com on IBMCloud Internet Services instance external-dns-public
    +

    $ ibmcloud cis domain-add example.com -i external-dns-public
    +

    +Follow step to active your zone

    +

    Private Zone

    +

    For this tutorial, we create private zone named example.com on IBMCloud DNS Services instance external-dns-private
    +

    $ ibmcloud dns zone-create example.com -i external-dns-private
    +

    +

    Creating configuration file

    +

    The preferred way to inject the configuration file is by using a Kubernetes secret. The secret should contain an object named azure.json with content similar to this:

    +
    {
    +  "apiKey": "1234567890abcdefghijklmnopqrstuvwxyz",
    +  "instanceCrn": "crn:v1:bluemix:public:internet-svcs:global:a/bcf1865e99742d38d2d5fc3fb80a5496:b950da8a-5be6-4691-810e-36388c77b0a3::"
    +}
    +
    +

    You can create or find the apiKey in your ibmcloud IAM → API Keys page

    +

    You can find the instanceCrn in your service instance details

    +

    Now you can create a file named ‘ibmcloud.json’ with values gathered above and with the structure of the example above. Use this file to create a Kubernetes secret:
    +

    $ kubectl create secret generic ibmcloud-config-file --from-file=/local/path/to/ibmcloud.json
    +

    +

    Deploy ExternalDNS

    +

    Connect your kubectl client to the cluster you want to test ExternalDNS with.
    +Then apply one of the following manifests file to deploy ExternalDNS.

    +

    Manifest (for clusters without RBAC enabled)

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service # ingress is also possible
    +        - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
    +        - --provider=ibmcloud
    +        - --ibmcloud-proxied # (optional) enable the proxy feature of IBMCloud
    +        volumeMounts:
    +        - name: ibmcloud-config-file
    +          mountPath: /etc/kubernetes
    +          readOnly: true
    +      volumes:
    +      - name: ibmcloud-config-file
    +        secret:
    +          secretName: ibmcloud-config-file
    +          items:
    +          - key: externaldns-config.json
    +            path: ibmcloud.json
    +
    +

    Manifest (for clusters with RBAC enabled)

    +
    apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRole
    +metadata:
    +  name: external-dns
    +rules:
    +- apiGroups: [""]
    +  resources: ["services","endpoints","pods"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: ["extensions","networking.k8s.io"]
    +  resources: ["ingresses"] 
    +  verbs: ["get","watch","list"]
    +- apiGroups: [""]
    +  resources: ["nodes"]
    +  verbs: ["list", "watch"]
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRoleBinding
    +metadata:
    +  name: external-dns-viewer
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: ClusterRole
    +  name: external-dns
    +subjects:
    +- kind: ServiceAccount
    +  name: external-dns
    +  namespace: default
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      serviceAccountName: external-dns
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service # ingress is also possible
    +        - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
    +        - --provider=ibmcloud
    +        - --ibmcloud-proxied # (optional) enable the proxy feature of IBMCloud public zone
    +        volumeMounts:
    +        - name: ibmcloud-config-file
    +          mountPath: /etc/kubernetes
    +          readOnly: true
    +      volumes:
    +      - name: ibmcloud-config-file
    +        secret:
    +          secretName: ibmcloud-config-file
    +          items:
    +          - key: externaldns-config.json
    +            path: ibmcloud.json
    +
    +

    Deploying an Nginx Service

    +

    Create a service file called nginx.yaml with the following contents:

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: nginx
    +spec:
    +  selector:
    +    matchLabels:
    +      app: nginx
    +  template:
    +    metadata:
    +      labels:
    +        app: nginx
    +    spec:
    +      containers:
    +      - image: nginx
    +        name: nginx
    +        ports:
    +        - containerPort: 80
    +---
    +apiVersion: v1
    +kind: Service
    +metadata:
    +  name: nginx
    +  annotations:
    +    external-dns.alpha.kubernetes.io/hostname: www.example.com
    +    external-dns.alpha.kubernetes.io/ttl: "120" #optional
    +spec:
    +  selector:
    +    app: nginx
    +  type: LoadBalancer
    +  ports:
    +    - protocol: TCP
    +      port: 80
    +      targetPort: 80
    +
    +

    Note the annotation on the service; use the hostname as the IBMCloud DNS zone created above. The annotation may also be a subdomain
    +of the DNS zone (e.g. ‘www.example.com’).

    +

    By setting the TTL annotation on the service, you have to pass a valid TTL, which must be 120 or above.
    +This annotation is optional, if you won’t set it, it will be 1 (automatic) which is 300.

    +

    ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation
    +will cause ExternalDNS to remove the corresponding DNS records.

    +

    Create the deployment and service:

    +
    $ kubectl create -f nginx.yaml
    +
    +

    Depending where you run your service it can take a little while for your cloud provider to create an external IP for the service.

    +

    Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize
    +the IBMCloud DNS records.

    +

    Verifying IBMCloud DNS records

    +

    Run the following command to view the A records:

    +

    Public Zone

    +
    # Get the domain ID with below command on IBMCloud Internet Services instance `external-dns-public`
    +$ ibmcloud cis domains -i external-dns-public
    +# Get the records with domain ID
    +$ ibmcloud cis dns-records DOMAIN_ID  -i external-dns-public
    +
    +

    Private Zone

    +

    # Get the domain ID with below command on IBMCloud DNS Services instance `external-dns-private`
    +$ ibmcloud dns zones -i external-dns-private
    +# Get the records with domain ID
    +$ ibmcloud dns resource-records ZONE_ID  -i external-dns-public
    +

    +This should show the external IP address of the service as the A record for your domain.

    +

    Cleanup

    +

    Now that we have verified that ExternalDNS will automatically manage IBMCloud DNS records, we can delete the tutorial’s example:

    +
    $ kubectl delete -f nginx.yaml
    +$ kubectl delete -f externaldns.yaml
    +
    +

    Setting proxied records on public zone

    +

    Using the external-dns.alpha.kubernetes.io/ibmcloud-proxied: "true" annotation on your ingress or service, you can specify if the proxy feature of IBMCloud public DNS should be enabled for that record. This setting will override the global --ibmcloud-proxied setting.

    +

    Active priviate zone with VPC allocated

    +

    By default, IBMCloud DNS Services don’t active your private zone with new zone added, with externale DNS, you can use external-dns.alpha.kubernetes.io/ibmcloud-vpc: "crn:v1:bluemix:public:is:us-south:a/bcf1865e99742d38d2d5fc3fb80a5496::vpc:r006-74353823-a60d-42e4-97c5-5e2551278435" annotation on your ingress or service, it will active your private zone with in specific VPC for that record created in. this setting won’t work if the private zone was active already.

    +

    Note: the annotaion value is the VPC CRN, every IBM Cloud service have a valid CRN.

    + +
    +
    + + + Last update: + May 29, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/infoblox/index.html b/v0.13.6/tutorials/infoblox/index.html new file mode 100644 index 0000000000..d5b22e9965 --- /dev/null +++ b/v0.13.6/tutorials/infoblox/index.html @@ -0,0 +1,2362 @@ + + + + + + + + + + + + + + + + + + Setting up ExternalDNS for Infoblox - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + + + + +
    +
    + + + + + + + + +

    Setting up ExternalDNS for Infoblox

    +

    This tutorial describes how to setup ExternalDNS for usage with Infoblox.

    +

    Make sure to use >=0.4.6 version of ExternalDNS for this tutorial. The only WAPI version that
    +has been validated is v2.3.1. It is assumed that the API user has rights to create objects of
    +the following types: zone_auth, record:a, record:cname, record:txt.

    +

    This tutorial assumes you have substituted the correct values for the following environment variables:

    +
    export GRID_HOST=127.0.0.1
    +export WAPI_PORT=443
    +export WAPI_VERSION=2.3.1
    +export WAPI_USERNAME=admin
    +export WAPI_PASSWORD=infoblox
    +
    +

    Creating an Infoblox DNS zone

    +

    The Infoblox provider for ExternalDNS will find suitable zones for domains it manages; it will
    +not automatically create zones.

    +

    Create an Infoblox DNS zone for “example.com”:

    +
    $ curl -kl \
    +      -X POST \
    +      -d fqdn=example.com \
    +      -u ${WAPI_USERNAME}:${WAPI_PASSWORD} \
    +         https://${GRID_HOST}:${WAPI_PORT}/wapi/v${WAPI_VERSION}/zone_auth
    +
    +

    Substitute a domain you own for “example.com” if desired.

    +

    Creating an Infoblox Configuration Secret

    +

    For ExternalDNS to access the Infoblox API, create a Kubernetes secret.

    +

    To create the secret:

    +
    $ kubectl create secret generic external-dns \
    +      --from-literal=EXTERNAL_DNS_INFOBLOX_WAPI_USERNAME=${WAPI_USERNAME} \
    +      --from-literal=EXTERNAL_DNS_INFOBLOX_WAPI_PASSWORD=${WAPI_PASSWORD}
    +
    +

    Deploy ExternalDNS

    +

    Connect your kubectl client to the cluster you want to test ExternalDNS with.
    +Then apply one of the following manifests file to deploy ExternalDNS.

    +

    Manifest (for clusters without RBAC enabled)

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service
    +        - --domain-filter=example.com       # (optional) limit to only example.com domains.
    +        - --provider=infoblox
    +        - --infoblox-grid-host=${GRID_HOST} # (required) IP of the Infoblox Grid host.
    +        - --infoblox-wapi-port=443          # (optional) Infoblox WAPI port. The default is "443".
    +        - --infoblox-wapi-version=2.3.1     # (optional) Infoblox WAPI version. The default is "2.3.1"
    +        - --infoblox-ssl-verify             # (optional) Use --no-infoblox-ssl-verify to skip server certificate verification.
    +        - --infoblox-create-ptr             # (optional) Use --infoblox-create-ptr to create a ptr entry in addition to an entry.
    +        env:
    +        - name: EXTERNAL_DNS_INFOBLOX_HTTP_POOL_CONNECTIONS
    +          value: "10" # (optional) Infoblox WAPI request connection pool size. The default is "10".
    +        - name: EXTERNAL_DNS_INFOBLOX_HTTP_REQUEST_TIMEOUT
    +          value: "60" # (optional) Infoblox WAPI request timeout in seconds. The default is "60".
    +        - name: EXTERNAL_DNS_INFOBLOX_WAPI_USERNAME
    +          valueFrom:
    +            secretKeyRef:
    +              name: external-dns
    +              key: EXTERNAL_DNS_INFOBLOX_WAPI_USERNAME
    +        - name: EXTERNAL_DNS_INFOBLOX_WAPI_PASSWORD
    +          valueFrom:
    +            secretKeyRef:
    +              name: external-dns
    +              key: EXTERNAL_DNS_INFOBLOX_WAPI_PASSWORD
    +
    +

    Manifest (for clusters with RBAC enabled)

    +
    apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRole
    +metadata:
    +  name: external-dns
    +rules:
    +- apiGroups: [""]
    +  resources: ["services","endpoints","pods"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: ["extensions","networking.k8s.io"]
    +  resources: ["ingresses"] 
    +  verbs: ["get","watch","list"]
    +- apiGroups: [""]
    +  resources: ["nodes"]
    +  verbs: ["list"]
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRoleBinding
    +metadata:
    +  name: external-dns-viewer
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: ClusterRole
    +  name: external-dns
    +subjects:
    +- kind: ServiceAccount
    +  name: external-dns
    +  namespace: default
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      serviceAccountName: external-dns
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service
    +        - --domain-filter=example.com       # (optional) limit to only example.com domains.
    +        - --provider=infoblox
    +        - --infoblox-grid-host=${GRID_HOST} # (required) IP of the Infoblox Grid host.
    +        - --infoblox-wapi-port=443          # (optional) Infoblox WAPI port. The default is "443".
    +        - --infoblox-wapi-version=2.3.1     # (optional) Infoblox WAPI version. The default is "2.3.1"
    +        - --infoblox-ssl-verify             # (optional) Use --no-infoblox-ssl-verify to skip server certificate verification.
    +        - --infoblox-create-ptr             # (optional) Use --infoblox-create-ptr to create a ptr entry in addition to an entry.
    +        env:
    +        - name: EXTERNAL_DNS_INFOBLOX_HTTP_POOL_CONNECTIONS
    +          value: "10" # (optional) Infoblox WAPI request connection pool size. The default is "10".
    +        - name: EXTERNAL_DNS_INFOBLOX_HTTP_REQUEST_TIMEOUT
    +          value: "60" # (optional) Infoblox WAPI request timeout in seconds. The default is "60".
    +        - name: EXTERNAL_DNS_INFOBLOX_WAPI_USERNAME
    +          valueFrom:
    +            secretKeyRef:
    +              name: external-dns
    +              key: EXTERNAL_DNS_INFOBLOX_WAPI_USERNAME
    +        - name: EXTERNAL_DNS_INFOBLOX_WAPI_PASSWORD
    +          valueFrom:
    +            secretKeyRef:
    +              name: external-dns
    +              key: EXTERNAL_DNS_INFOBLOX_WAPI_PASSWORD
    +
    +

    Deploying an Nginx Service

    +

    Create a service file called ‘nginx.yaml’ with the following contents:

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: nginx
    +spec:
    +  selector:
    +    matchLabels:
    +      app: nginx
    +  template:
    +    metadata:
    +      labels:
    +        app: nginx
    +    spec:
    +      containers:
    +      - image: nginx
    +        name: nginx
    +        ports:
    +        - containerPort: 80
    +---
    +apiVersion: v1
    +kind: Service
    +metadata:
    +  name: nginx
    +  annotations:
    +    external-dns.alpha.kubernetes.io/hostname: example.com
    +spec:
    +  selector:
    +    app: nginx
    +  type: LoadBalancer
    +  ports:
    +    - protocol: TCP
    +      port: 80
    +      targetPort: 80
    +
    +

    Note the annotation on the service; use the same hostname as the Infoblox DNS zone created above. The annotation may also be a subdomain
    +of the DNS zone (e.g. ‘www.example.com’).

    +

    ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation
    +will cause ExternalDNS to remove the corresponding DNS records.

    +

    Create the deployment and service:

    +
    $ kubectl create -f nginx.yaml
    +
    +

    It takes a little while for the Infoblox cloud provider to create an external IP for the service. Check the status by running
    +kubectl get services nginx. If the EXTERNAL-IP field shows an address, the service is ready to be accessed externally.

    +

    Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize
    +the Infoblox DNS records.

    +

    Verifying Infoblox DNS records

    +

    Run the following command to view the A records for your Infoblox DNS zone:

    +
    $ curl -kl \
    +      -X GET \
    +      -u ${WAPI_USERNAME}:${WAPI_PASSWORD} \
    +         https://${GRID_HOST}:${WAPI_PORT}/wapi/v${WAPI_VERSION}/record:a?zone=example.com
    +
    +

    Substitute the zone for the one created above if a different domain was used.

    +

    This should show the external IP address of the service as the A record for your domain (‘@’ indicates the record is for the zone itself).

    +

    Clean up

    +

    Now that we have verified that ExternalDNS will automatically manage Infoblox DNS records, we can delete the tutorial’s
    +DNS zone:

    +
    $ curl -kl \
    +      -X DELETE \
    +      -u ${WAPI_USERNAME}:${WAPI_PASSWORD} \
    +         https://${GRID_HOST}:${WAPI_PORT}/wapi/v${WAPI_VERSION}/zone_auth?fqdn=example.com
    +
    +

    Ability to filter results from the zone auth API using a regular expression

    +

    There is also the ability to filter results from the Infoblox zone_auth service based upon a regular expression. See the Infoblox API document for examples. To use this feature for the zone_auth service, set the parameter infoblox-fqdn-regex for external-dns to a regular expression that makes sense for you. For instance, to only return hosted zones that start with staging in the test.com domain (like staging.beta.test.com, or staging.test.com), use the following command line option when starting external-dns

    +
    --infoblox-fqdn-regex=^staging.*test.com$
    +
    +

    Ability to filter A, Host, CNAME and TXT records from the by name using a regular expression

    +

    Infoblox supports filtering records by name using a regular expression. See the Infoblox API document for examples. To use this feature, set the parameter infoblox-name-regex for external-dns to a regular expression that makes sense for you. For instance, if all your dns records end with cluster1.example.com, you can fetch records matching this style by setting the following:

    +
    --infoblox-name-regex=cluster1.example.com
    +
    +

    Infoblox PTR record support

    +

    There is an option to enable PTR records support for infoblox provider. PTR records allow to do reverse dns search. To enable PTR records support, add following into arguments for external-dns:
    +--infoblox-create-ptr to allow management of PTR records.
    +You can also add a filter for reverse dns zone to limit PTR records to specific zones only:
    +--domain-filter=10.196.0.0/16 change this to the reverse zone(s) as defined in your infoblox.
    +Now external-dns will manage PTR records for you.

    + +
    +
    + + + Last update: + May 29, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/istio/index.html b/v0.13.6/tutorials/istio/index.html new file mode 100644 index 0000000000..be9fc4a5b6 --- /dev/null +++ b/v0.13.6/tutorials/istio/index.html @@ -0,0 +1,2487 @@ + + + + + + + + + + + + + + + + + + Configuring ExternalDNS to use the Istio Gateway and/or Istio Virtual Service Source - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + + + + +
    +
    + + + + + + + + +

    Configuring ExternalDNS to use the Istio Gateway and/or Istio Virtual Service Source

    +

    This tutorial describes how to configure ExternalDNS to use the Istio Gateway source.
    +It is meant to supplement the other provider-specific setup tutorials.

    +

    Note: Using the Istio Gateway source requires Istio >=1.0.0.

    +
      +
    • Manifest (for clusters without RBAC enabled)
    • +
    • Manifest (for clusters with RBAC enabled)
    • +
    • Update existing ExternalDNS Deployment
    • +
    +

    Manifest (for clusters without RBAC enabled)

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service
    +        - --source=ingress
    +        - --source=istio-gateway        # choose one
    +        - --source=istio-virtualservice # or both
    +        - --domain-filter=external-dns-test.my-org.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones
    +        - --provider=aws
    +        - --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization
    +        - --aws-zone-type=public # only look at public hosted zones (valid values are public, private or no value for both)
    +        - --registry=txt
    +        - --txt-owner-id=my-identifier
    +
    +

    Manifest (for clusters with RBAC enabled)

    +
    apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRole
    +metadata:
    +  name: external-dns
    +rules:
    +- apiGroups: [""]
    +  resources: ["services","endpoints","pods"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: ["extensions","networking.k8s.io"]
    +  resources: ["ingresses"] 
    +  verbs: ["get","watch","list"]
    +- apiGroups: [""]
    +  resources: ["nodes"]
    +  verbs: ["list"]
    +- apiGroups: ["networking.istio.io"]
    +  resources: ["gateways", "virtualservices"]
    +  verbs: ["get","watch","list"]
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRoleBinding
    +metadata:
    +  name: external-dns-viewer
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: ClusterRole
    +  name: external-dns
    +subjects:
    +- kind: ServiceAccount
    +  name: external-dns
    +  namespace: default
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      serviceAccountName: external-dns
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service
    +        - --source=ingress
    +        - --source=istio-gateway
    +        - --source=istio-virtualservice
    +        - --domain-filter=external-dns-test.my-org.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones
    +        - --provider=aws
    +        - --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization
    +        - --aws-zone-type=public # only look at public hosted zones (valid values are public, private or no value for both)
    +        - --registry=txt
    +        - --txt-owner-id=my-identifier
    +
    +

    Update existing ExternalDNS Deployment

    +
      +
    • For clusters with running external-dns, you can just update the deployment.
    • +
    • With access to the kube-system namespace, update the existing external-dns deployment.
    • +
    • Add a parameter to the arguments of the container to create dns entries with --source=istio-gateway.
    • +
    +

    Execute the following command or update the argument.

    +
    kubectl patch deployment external-dns --type='json' \
    +  -p='[{"op": "add", "path": "/spec/template/spec/containers/0/args/2", "value": "--source=istio-gateway" }]'
    +
    +

    In case the setup uses a clusterrole, just append a new value to the enable the istio group.

    +
    kubectl patch clusterrole external-dns --type='json' \
    +  -p='[{"op": "add", "path": "/rules/4", "value": { "apiGroups": [ "networking.istio.io"], "resources": ["gateways"],"verbs": ["get", "watch", "list" ]} }]'
    +
    +

    Verify that Istio Gateway/VirtualService Source works

    +

    Follow the Istio ingress traffic tutorial
    +to deploy a sample service that will be exposed outside of the service mesh.
    +The following are relevant snippets from that tutorial.

    +

    Install a sample service

    +

    With automatic sidecar injection:
    +

    $ kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.6/samples/httpbin/httpbin.yaml
    +

    +

    Otherwise:
    +

    $ kubectl apply -f <(istioctl kube-inject -f https://raw.githubusercontent.com/istio/istio/release-1.6/samples/httpbin/httpbin.yaml)
    +

    +

    Using a Gateway as a source

    +
    Create an Istio Gateway:
    +
    $ cat <<EOF | kubectl apply -f -
    +apiVersion: networking.istio.io/v1alpha3
    +kind: Gateway
    +metadata:
    +  name: httpbin-gateway
    +  namespace: istio-system
    +spec:
    +  selector:
    +    istio: ingressgateway # use Istio default gateway implementation
    +  servers:
    +  - port:
    +      number: 80
    +      name: http
    +      protocol: HTTP
    +    hosts:
    +    - "httpbin.example.com" # this is used by external-dns to extract DNS names
    +EOF
    +
    +
    Configure routes for traffic entering via the Gateway:
    +
    $ cat <<EOF | kubectl apply -f -
    +apiVersion: networking.istio.io/v1alpha3
    +kind: VirtualService
    +metadata:
    +  name: httpbin
    +spec:
    +  hosts:
    +  - "httpbin.example.com"
    +  gateways:
    +  - istio-system/httpbin-gateway
    +  http:
    +  - match:
    +    - uri:
    +        prefix: /status
    +    - uri:
    +        prefix: /delay
    +    route:
    +    - destination:
    +        port:
    +          number: 8000
    +        host: httpbin
    +EOF
    +
    +

    Using a VirtualService as a source

    +
    Create an Istio Gateway:
    +
    $ cat <<EOF | kubectl apply -f -
    +apiVersion: networking.istio.io/v1alpha3
    +kind: Gateway
    +metadata:
    +  name: httpbin-gateway
    +  namespace: istio-system
    +spec:
    +  selector:
    +    istio: ingressgateway # use Istio default gateway implementation
    +  servers:
    +  - port:
    +      number: 80
    +      name: http
    +      protocol: HTTP
    +    hosts:
    +    - "*"
    +EOF
    +
    +
    Configure routes for traffic entering via the Gateway:
    +
    $ cat <<EOF | kubectl apply -f -
    +apiVersion: networking.istio.io/v1alpha3
    +kind: VirtualService
    +metadata:
    +  name: httpbin
    +spec:
    +  hosts:
    +  - "httpbin.example.com" # this is used by external-dns to extract DNS names
    +  gateways:
    +  - istio-system/httpbin-gateway
    +  http:
    +  - match:
    +    - uri:
    +        prefix: /status
    +    - uri:
    +        prefix: /delay
    +    route:
    +    - destination:
    +        port:
    +          number: 8000
    +        host: httpbin
    +EOF
    +
    +

    Access the sample service using curl

    +
    $ curl -I http://httpbin.example.com/status/200
    +HTTP/1.1 200 OK
    +server: envoy
    +date: Tue, 28 Aug 2018 15:26:47 GMT
    +content-type: text/html; charset=utf-8
    +access-control-allow-origin: *
    +access-control-allow-credentials: true
    +content-length: 0
    +x-envoy-upstream-service-time: 5
    +
    +

    Accessing any other URL that has not been explicitly exposed should return an HTTP 404 error:
    +

    $ curl -I http://httpbin.example.com/headers
    +HTTP/1.1 404 Not Found
    +date: Tue, 28 Aug 2018 15:27:48 GMT
    +server: envoy
    +transfer-encoding: chunked
    +

    +

    Note: The -H flag in the original Istio tutorial is no longer necessary in the curl commands.

    +

    Optional Gateway Annotation

    +

    To support setups where an Ingress resource is used provision an external LB you can add the following annotation to your Gateway

    +

    Note: The Ingress namespace can be omitted if its in the same namespace as the gateway

    +
    $ cat <<EOF | kubectl apply -f -
    +apiVersion: networking.istio.io/v1alpha3
    +kind: Gateway
    +metadata:
    +  name: httpbin-gateway
    +  namespace: istio-system
    +  annotations:
    +    "external-dns.alpha.kubernetes.io/ingress": "$ingressNamespace/$ingressName"
    +spec:
    +  selector:
    +    istio: ingressgateway # use Istio default gateway implementation
    +  servers:
    +  - port:
    +      number: 80
    +      name: http
    +      protocol: HTTP
    +    hosts:
    +    - "*"
    +EOF
    +
    +

    Debug ExternalDNS

    +
      +
    • Look for the deployment pod to see the status
    • +
    +

    ```console$ kubectl get pods | grep external-dns
    +external-dns-6b84999479-4knv9 1/1 Running 0 3h29m
    +

    * Watch for the logs as follows
    +
    +```console
    +$ kubectl logs -f external-dns-6b84999479-4knv9
    +

    +

    At this point, you can create or update any Istio Gateway object with hosts entries array.

    +
    +

    ATTENTION: Make sure to specify those whose account is related to the DNS record.

    +
    +
      +
    • Successful executions will print the following
    • +
    +
    time="2020-01-17T06:08:08Z" level=info msg="Desired change: CREATE httpbin.example.com A"
    +time="2020-01-17T06:08:08Z" level=info msg="Desired change: CREATE httpbin.example.com TXT"
    +time="2020-01-17T06:08:08Z" level=info msg="2 record(s) in zone example.com. were successfully updated"
    +time="2020-01-17T06:09:08Z" level=info msg="All records are already up to date, there are no changes for the matching hosted zones"
    +
    +
      +
    • If there’s any problem around clusterrole, you would see the errors showing wrong permissions:
    • +
    +
    source \"gateways\" in API group \"networking.istio.io\" at the cluster scope"
    +time="2020-01-17T06:07:08Z" level=error msg="gateways.networking.istio.io is forbidden: User \"system:serviceaccount:kube-system:external-dns\" cannot list resource \"gateways\" in API group \"networking.istio.io\" at the cluster scope"
    +
    + +
    +
    + + + Last update: + August 8, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/kong/index.html b/v0.13.6/tutorials/kong/index.html new file mode 100644 index 0000000000..c147d16f93 --- /dev/null +++ b/v0.13.6/tutorials/kong/index.html @@ -0,0 +1,2081 @@ + + + + + + + + + + + + + + + + + + Configuring ExternalDNS to use the Kong TCPIngress Source - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + +
    +
    +
    + + + +
    +
    +
    + + +
    +
    + + + + + + + + +

    Configuring ExternalDNS to use the Kong TCPIngress Source

    +

    This tutorial describes how to configure ExternalDNS to use the Kong TCPIngress source.
    +It is meant to supplement the other provider-specific setup tutorials.

    +

    Manifest (for clusters without RBAC enabled)

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      containers:
    +      - name: external-dns
    +        # update this to the desired external-dns version
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=kong-tcpingress
    +        - --provider=aws
    +        - --registry=txt
    +        - --txt-owner-id=my-identifier
    +
    +

    Manifest (for clusters with RBAC enabled)

    +

    Could be changed if you have mulitple sources

    +
    apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRole
    +metadata:
    +  name: external-dns
    +rules:
    +- apiGroups: [""]
    +  resources: ["services","endpoints","pods"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: [""]
    +  resources: ["nodes"]
    +  verbs: ["list","watch"]
    +- apiGroups: ["configuration.konghq.com"]
    +  resources: ["tcpingresses"]
    +  verbs: ["get","watch","list"]
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRoleBinding
    +metadata:
    +  name: external-dns-viewer
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: ClusterRole
    +  name: external-dns
    +subjects:
    +- kind: ServiceAccount
    +  name: external-dns
    +  namespace: default
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      serviceAccountName: external-dns
    +      containers:
    +      - name: external-dns
    +        # update this to the desired external-dns version
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=kong-tcpingress
    +        - --provider=aws
    +        - --registry=txt
    +        - --txt-owner-id=my-identifier
    +
    + +
    +
    + + + Last update: + May 29, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/kops-dns-controller/index.html b/v0.13.6/tutorials/kops-dns-controller/index.html new file mode 100644 index 0000000000..0ab5d5839f --- /dev/null +++ b/v0.13.6/tutorials/kops-dns-controller/index.html @@ -0,0 +1,2063 @@ + + + + + + + + + + + + + + + + + + kOps dns-controller compatibility mode - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + +
    +
    +
    + + + +
    +
    +
    + + +
    +
    + + + + + + + + +

    kOps dns-controller compatibility mode

    +

    kOps includes a dns-controller that is primarily used to bootstrap the cluster, but can also be used for provisioning DNS entries for Services and Ingress.

    +

    ExternalDNS can be used as a drop-in replacement for dns-controller if you are running a non-gossip cluster. The flag --compatibility kops-dns-controller enables the dns-controller behaviour.

    +

    Annotations

    +

    In kops-dns-controller compatibility mode, ExternalDNS supports two additional annotations:

    +
      +
    • +

      dns.alpha.kubernetes.io/external which is used to define a DNS record for accessing the resource publicly (i.e. public IPs)

      +
    • +
    • +

      dns.alpha.kubernetes.io/internal which is used to define a DNS record for accessing the resource from outside the cluster but inside the cloud,
      +i.e. it will typically use internal IPs for instances.

      +
    • +
    +

    These annotations may both be comma-separated lists of names.

    +

    DNS record mappings

    +

    The DNS record mappings try to “do the right thing”, but what this means is different for each resource type.

    +

    Pods

    +

    For the external annotation, ExternalDNS will map a HostNetwork=true Pod to the external IPs of the Node.

    +

    For the internal annotation, ExternalDNS will map a HostNetwork=true Pod to the internal IPs of the Node.

    +

    ExternalDNS ignore Pods that are not HostNetwork=true

    +

    Annotations added to Pods will always result in an A record being created.

    +

    Services

    +
      +
    • +

      For a Service of Type=LoadBalancer, ExternalDNS looks at Status.LoadBalancer.Ingress. It will create CNAMEs to hostnames,
      + and A records for IP addresses. It will do this for both internal and external names

      +
    • +
    • +

      For a Service of Type=NodePort, ExternalDNS will create A records for the Node’s internal/external IP addresses, as appropriate.

      +
    • +
    + +
    +
    + + + Last update: + June 24, 2021 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/kube-ingress-aws/index.html b/v0.13.6/tutorials/kube-ingress-aws/index.html new file mode 100644 index 0000000000..f7dd10c934 --- /dev/null +++ b/v0.13.6/tutorials/kube-ingress-aws/index.html @@ -0,0 +1,2335 @@ + + + + + + + + + + + + + + + + + + Using ExternalDNS with kube-ingress-aws-controller - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + +
    +
    + +
    +
    + + +
    +
    + + + + + + + + +

    Using ExternalDNS with kube-ingress-aws-controller

    +

    This tutorial describes how to use ExternalDNS with the kube-ingress-aws-controller.

    +

    Setting up ExternalDNS and kube-ingress-aws-controller

    +

    Follow the AWS tutorial to setup ExternalDNS for use in Kubernetes clusters
    +running in AWS. Specify the source=ingress argument so that ExternalDNS will look
    +for hostnames in Ingress objects. In addition, you may wish to limit which Ingress
    +objects are used as an ExternalDNS source via the ingress-class argument, but
    +this is not required.

    +

    For help setting up the Kubernetes Ingress AWS Controller, that can
    +create ALBs and NLBs, follow the Setup Guide.

    +

    Optional RouteGroup

    +

    RouteGroup is a CRD, that enables you to do complex routing with
    +Skipper.

    +

    First, you have to apply the RouteGroup CRD to your cluster:

    +
    kubectl apply -f https://github.com/zalando/skipper/blob/HEAD/dataclients/kubernetes/deploy/apply/routegroups_crd.yaml
    +
    +

    You have to grant all controllers: Skipper,
    +kube-ingress-aws-controller and external-dns to read the routegroup resource and
    +kube-ingress-aws-controller to update the status field of a routegroup.
    +This depends on your RBAC policies, in case you use RBAC, you can use
    +this for all 3 controllers:

    +
    apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRole
    +metadata:
    +  name: kube-ingress-aws-controller
    +rules:
    +- apiGroups:
    +  - extensions
    +  - networking.k8s.io
    +  resources:
    +  - ingresses
    +  verbs:
    +  - get
    +  - list
    +  - watch
    +- apiGroups:
    +  - extensions
    +  - networking.k8s.io
    +  resources:
    +  - ingresses/status
    +  verbs:
    +  - patch
    +  - update
    +- apiGroups:
    +  - zalando.org
    +  resources:
    +  - routegroups
    +  verbs:
    +  - get
    +  - list
    +  - watch
    +- apiGroups:
    +  - zalando.org
    +  resources:
    +  - routegroups/status
    +  verbs:
    +  - patch
    +  - update
    +
    +

    See also current RBAC yaml files:
    +- kube-ingress-aws-controller
    +- skipper
    +- external-dns

    +

    Deploy an example application

    +

    Create the following sample “echoserver” application to demonstrate how
    +ExternalDNS works with ingress objects, that were created by kube-ingress-aws-controller.

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: echoserver
    +spec:
    +  replicas: 1
    +  selector:
    +    matchLabels:
    +      app: echoserver
    +  template:
    +    metadata:
    +      labels:
    +        app: echoserver
    +    spec:
    +      containers:
    +      - image: gcr.io/google_containers/echoserver:1.4
    +        imagePullPolicy: Always
    +        name: echoserver
    +        ports:
    +        - containerPort: 8080
    +---
    +apiVersion: v1
    +kind: Service
    +metadata:
    +  name: echoserver
    +spec:
    +  ports:
    +    - port: 80
    +      targetPort: 8080
    +      protocol: TCP
    +  type: ClusterIP
    +  selector:
    +    app: echoserver
    +
    +

    Note that the Service object is of type ClusterIP, because we will
    +target Skipper and do the HTTP routing in Skipper. We don’t need
    +a Service of type LoadBalancer here, since we will be using a shared
    +skipper-ingress for all Ingress. Skipper use hostNetwork to be able
    +to get traffic from AWS LoadBalancers EC2 network. ALBs or NLBs, will
    +be created based on need and will be shared across all ingress as
    +default.

    +

    Ingress examples

    +

    Create the following Ingress to expose the echoserver application to the Internet.

    +
    apiVersion: networking.k8s.io/v1
    +kind: Ingress
    +metadata:
    +  name: echoserver
    +spec:
    +  ingressClassName: skipper
    +  rules:
    +  - host: echoserver.mycluster.example.org
    +    http: &echoserver_root
    +      paths:
    +      - path: /
    +        backend:
    +          service:
    +            name: echoserver
    +            port:
    +              number: 80
    +        pathType: Prefix
    +  - host: echoserver.example.org
    +    http: *echoserver_root
    +
    +

    The above should result in the creation of an (ipv4) ALB in AWS which will forward
    +traffic to skipper which will forward to the echoserver application.

    +

    If the --source=ingress argument is specified, then ExternalDNS will create DNS
    +records based on the hosts specified in ingress objects. The above example would
    +result in two alias records being created, echoserver.mycluster.example.org and
    +echoserver.example.org, which both alias the ALB that is associated with the
    +Ingress object.

    +

    Note that the above example makes use of the YAML anchor feature to avoid having
    +to repeat the http section for multiple hosts that use the exact same paths. If
    +this Ingress object will only be fronting one backend Service, we might instead
    +create the following:

    +
    apiVersion: networking.k8s.io/v1
    +kind: Ingress
    +metadata:
    +  annotations:
    +    external-dns.alpha.kubernetes.io/hostname: echoserver.mycluster.example.org, echoserver.example.org
    +  name: echoserver
    +spec:
    +  ingressClassName: skipper
    +  rules:
    +  - http:
    +      paths:
    +      - path: /
    +        backend:
    +          service:
    +            name: echoserver
    +            port:
    +              number: 80
    +        pathType: Prefix
    +
    +

    In the above example we create a default path that works for any hostname, and
    +make use of the external-dns.alpha.kubernetes.io/hostname annotation to create
    +multiple aliases for the resulting ALB.

    +

    Dualstack ALBs

    +

    AWS supports both IPv4 and “dualstack” (both IPv4 and IPv6) interfaces for ALBs.
    +The Kubernetes Ingress AWS controller supports the alb.ingress.kubernetes.io/ip-address-type
    +annotation (which defaults to ipv4) to determine this. If this annotation is
    +set to dualstack then ExternalDNS will create two alias records (one A record
    +and one AAAA record) for each hostname associated with the Ingress object.

    +

    Example:

    +
    apiVersion: networking.k8s.io/v1
    +kind: Ingress
    +metadata:
    +  annotations:
    +    alb.ingress.kubernetes.io/ip-address-type: dualstack
    +  name: echoserver
    +spec:
    +  ingressClassName: skipper
    +  rules:
    +  - host: echoserver.example.org
    +    http:
    +      paths:
    +      - path: /
    +        backend:
    +          service:
    +            name: echoserver
    +            port:
    +              number: 80
    +        pathType: Prefix
    +
    +

    The above Ingress object will result in the creation of an ALB with a dualstack
    +interface. ExternalDNS will create both an A echoserver.example.org record and
    +an AAAA record of the same name, that each are aliases for the same ALB.

    +

    NLBs

    +

    AWS has
    +NLBs
    +and kube-ingress-aws-controller is able to create NLBs instead of ALBs.
    +The Kubernetes Ingress AWS controller supports the zalando.org/aws-load-balancer-type
    +annotation (which defaults to alb) to determine this. If this annotation is
    +set to nlb then ExternalDNS will create an NLB instead of an ALB.

    +

    Example:

    +
    apiVersion: networking.k8s.io/v1
    +kind: Ingress
    +metadata:
    +  annotations:
    +    zalando.org/aws-load-balancer-type: nlb
    +  name: echoserver
    +spec:
    +  ingressClassName: skipper
    +  rules:
    +  - host: echoserver.example.org
    +    http:
    +      paths:
    +      - path: /
    +        backend:
    +          service:
    +            name: echoserver
    +            port:
    +              number: 80
    +        pathType: Prefix
    +
    +

    The above Ingress object will result in the creation of an NLB. A
    +successful create, you can observe in the ingress status field, that is
    +written by kube-ingress-aws-controller:

    +
    status:
    +  loadBalancer:
    +    ingress:
    +    - hostname: kube-ing-lb-atedkrlml7iu-1681027139.$region.elb.amazonaws.com
    +
    +

    ExternalDNS will create a A-records echoserver.example.org, that
    +use AWS ALIAS record to automatically maintain IP addresses of the NLB.

    +

    RouteGroup (optional)

    +

    Kube-ingress-aws-controller, Skipper and external-dns
    +support RouteGroups. External-dns needs to be started with
    +--source=skipper-routegroup parameter in order to work on RouteGroup objects.

    +

    Here we can not show all RouteGroup
    +capabilities
    ,
    +but we show one simple example with an application and a custom https
    +redirect.

    +
    apiVersion: zalando.org/v1
    +kind: RouteGroup
    +metadata:
    +  name: my-route-group
    +spec:
    +  backends:
    +  - name: my-backend
    +    type: service
    +    serviceName: my-service
    +    servicePort: 80
    +  - name: redirectShunt
    +    type: shunt
    +  defaultBackends:
    +  - backendName: my-service
    +  routes:
    +  - pathSubtree: /
    +  - pathSubtree: /
    +    predicates:
    +    - Header("X-Forwarded-Proto", "http")
    +    filters:
    +    - redirectTo(302, "https:")
    +    backends:
    +    - redirectShunt
    +
    + +
    +
    + + + Last update: + May 5, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/linode/index.html b/v0.13.6/tutorials/linode/index.html new file mode 100644 index 0000000000..fda3a9b990 --- /dev/null +++ b/v0.13.6/tutorials/linode/index.html @@ -0,0 +1,2239 @@ + + + + + + + + + + + + + + + + + + Setting up ExternalDNS for Services on Linode - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + + + + +
    +
    + + + + + + + + +

    Setting up ExternalDNS for Services on Linode

    +

    This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using Linode DNS Manager.

    +

    Make sure to use >=0.5.5 version of ExternalDNS for this tutorial.

    +

    Managing DNS with Linode

    +

    If you want to learn about how to use Linode DNS Manager read the following tutorials:

    +

    An Introduction to Managing DNS, and general documentation

    +

    Creating Linode Credentials

    +

    Generate a new oauth token by following the instructions at Access-and-Authentication

    +

    The environment variable LINODE_TOKEN will be needed to run ExternalDNS with Linode.

    +

    Deploy ExternalDNS

    +

    Connect your kubectl client to the cluster you want to test ExternalDNS with.
    +Then apply one of the following manifests file to deploy ExternalDNS.

    +

    Manifest (for clusters without RBAC enabled)

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service # ingress is also possible
    +        - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
    +        - --provider=linode
    +        env:
    +        - name: LINODE_TOKEN
    +          value: "YOUR_LINODE_API_KEY"
    +
    +

    Manifest (for clusters with RBAC enabled)

    +
    apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRole
    +metadata:
    +  name: external-dns
    +rules:
    +- apiGroups: [""]
    +  resources: ["services","endpoints","pods"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: ["extensions","networking.k8s.io"]
    +  resources: ["ingresses"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: [""]
    +  resources: ["nodes"]
    +  verbs: ["list"]
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRoleBinding
    +metadata:
    +  name: external-dns-viewer
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: ClusterRole
    +  name: external-dns
    +subjects:
    +- kind: ServiceAccount
    +  name: external-dns
    +  namespace: default
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      serviceAccountName: external-dns
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service # ingress is also possible
    +        - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
    +        - --provider=linode
    +        env:
    +        - name: LINODE_TOKEN
    +          value: "YOUR_LINODE_API_KEY"
    +
    +

    Deploying an Nginx Service

    +

    Create a service file called ‘nginx.yaml’ with the following contents:

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: nginx
    +spec:
    +  selector:
    +    matchLabels:
    +      app: nginx
    +  template:
    +    metadata:
    +      labels:
    +        app: nginx
    +    spec:
    +      containers:
    +      - image: nginx
    +        name: nginx
    +        ports:
    +        - containerPort: 80
    +---
    +apiVersion: v1
    +kind: Service
    +metadata:
    +  name: nginx
    +  annotations:
    +    external-dns.alpha.kubernetes.io/hostname: my-app.example.com
    +spec:
    +  selector:
    +    app: nginx
    +  type: LoadBalancer
    +  ports:
    +    - protocol: TCP
    +      port: 80
    +      targetPort: 80
    +
    +

    Note the annotation on the service; use the same hostname as the Linode DNS zone created above.

    +

    ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records.

    +

    Create the deployment and service:

    +
    $ kubectl create -f nginx.yaml
    +
    +

    Depending where you run your service it can take a little while for your cloud provider to create an external IP for the service.

    +

    Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize the Linode DNS records.

    +

    Verifying Linode DNS records

    +

    Check your Linode UI to view the records for your Linode DNS zone.

    +

    Click on the zone for the one created above if a different domain was used.

    +

    This should show the external IP address of the service as the A record for your domain.

    +

    Cleanup

    +

    Now that we have verified that ExternalDNS will automatically manage Linode DNS records, we can delete the tutorial’s example:

    +
    $ kubectl delete service -f nginx.yaml
    +$ kubectl delete service -f externaldns.yaml
    +
    + +
    +
    + + + Last update: + May 29, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/mx-record/index.html b/v0.13.6/tutorials/mx-record/index.html new file mode 100644 index 0000000000..f2ec734dd0 --- /dev/null +++ b/v0.13.6/tutorials/mx-record/index.html @@ -0,0 +1,1952 @@ + + + + + + + + + + + + + + + + + + Creating MX record with CRD source - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + +
    +
    +
    + + + +
    +
    +
    + + +
    +
    + + + + + + + + +

    Creating MX record with CRD source

    +

    You can create and manage MX records with the help of CRD source
    +and DNSEndpoint CRD. Currently, this feature is only supported by aws, azure, and google providers.

    +

    In order to start managing MX records you need to set the --managed-record-types MX flag.

    +
    external-dns --source crd --provider {aws|azure|google} --managed-record-types A --managed-record-types CNAME --managed-record-types MX
    +
    +

    Targets within the CRD need to be specified according to the RFC 1034 (section 3.6.1). Below is an example of
    +example.com DNS MX record which specifies two separate targets with distinct priorities.

    +
    apiVersion: externaldns.k8s.io/v1alpha1
    +kind: DNSEndpoint
    +metadata:
    +  name: examplemxrecord
    +spec:
    +  endpoints:
    +    - dnsName: example.com
    +      recordTTL: 180
    +      recordType: MX
    +      targets:
    +        - 10 mailhost1.example.com
    +        - 20 mailhost2.example.com
    +
    + +
    +
    + + + Last update: + May 2, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/nginx-ingress/index.html b/v0.13.6/tutorials/nginx-ingress/index.html new file mode 100644 index 0000000000..15f0a195e3 --- /dev/null +++ b/v0.13.6/tutorials/nginx-ingress/index.html @@ -0,0 +1,2782 @@ + + + + + + + + + + + + + + + + + + Setting up ExternalDNS on GKE with nginx-ingress-controller - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + + + + +
    +
    + + + + + + + + +

    Setting up ExternalDNS on GKE with nginx-ingress-controller

    +

    This tutorial describes how to setup ExternalDNS for usage within a GKE cluster that doesn’t make use of Google’s default ingress controller but rather uses nginx-ingress-controller for that task.

    +

    Set up your environment

    +

    Setup your environment to work with Google Cloud Platform. Fill in your values as needed, e.g. target project.

    +
    $ gcloud config set project "zalando-external-dns-test"
    +$ gcloud config set compute/region "europe-west1"
    +$ gcloud config set compute/zone "europe-west1-d"
    +
    +

    GKE Node Scopes

    +

    The following instructions use instance scopes to provide ExternalDNS with the
    +permissions it needs to manage DNS records. Note that since these permissions
    +are associated with the instance, all pods in the cluster will also have these
    +permissions. As such, this approach is not suitable for anything but testing
    +environments.

    +

    Create a GKE cluster without using the default ingress controller.

    +
    $ gcloud container clusters create "external-dns" \
    +    --num-nodes 1 \
    +    --scopes "https://www.googleapis.com/auth/ndev.clouddns.readwrite"
    +
    +

    Create a DNS zone which will contain the managed DNS records.

    +
    $ gcloud dns managed-zones create "external-dns-test-gcp-zalan-do" \
    +    --dns-name "external-dns-test.gcp.zalan.do." \
    +    --description "Automatically managed zone by ExternalDNS"
    +
    +

    Make a note of the nameservers that were assigned to your new zone.

    +
    $ gcloud dns record-sets list \
    +    --zone "external-dns-test-gcp-zalan-do" \
    +    --name "external-dns-test.gcp.zalan.do." \
    +    --type NS
    +NAME                             TYPE  TTL    DATA
    +external-dns-test.gcp.zalan.do.  NS    21600  ns-cloud-e1.googledomains.com.,ns-cloud-e2.googledomains.com.,ns-cloud-e3.googledomains.com.,ns-cloud-e4.googledomains.com.
    +
    +

    In this case it’s ns-cloud-{e1-e4}.googledomains.com. but your’s could slightly differ, e.g. {a1-a4}, {b1-b4} etc.

    +

    Tell the parent zone where to find the DNS records for this zone by adding the corresponding NS records there. Assuming the parent zone is “gcp-zalan-do” and the domain is “gcp.zalan.do” and that it’s also hosted at Google we would do the following.

    +
    $ gcloud dns record-sets transaction start --zone "gcp-zalan-do"
    +$ gcloud dns record-sets transaction add ns-cloud-e{1..4}.googledomains.com. \
    +    --name "external-dns-test.gcp.zalan.do." --ttl 300 --type NS --zone "gcp-zalan-do"
    +$ gcloud dns record-sets transaction execute --zone "gcp-zalan-do"
    +
    +

    Connect your kubectl client to the cluster you just created and bind your GCP
    +user to the cluster admin role in Kubernetes.

    +
    $ gcloud container clusters get-credentials "external-dns"
    +$ kubectl create clusterrolebinding cluster-admin-me \
    +    --clusterrole=cluster-admin --user="$(gcloud config get-value account)"
    +
    +

    Deploy the nginx ingress controller

    +

    First, you need to deploy the nginx-based ingress controller. It can be deployed in at least two modes: Leveraging a Layer 4 load balancer in front of the nginx proxies or directly targeting pods with hostPorts on your worker nodes. ExternalDNS doesn’t really care and supports both modes.

    +

    Default Backend

    +

    The nginx controller uses a default backend that it serves when no Ingress rule matches. This is a separate Service that can be picked by you. We’ll use the default backend that’s used by other ingress controllers for that matter. Apply the following manifests to your cluster to deploy the default backend.

    +
    apiVersion: v1
    +kind: Service
    +metadata:
    +  name: default-http-backend
    +spec:
    +  ports:
    +  - port: 80
    +    targetPort: 8080
    +  selector:
    +    app: default-http-backend
    +
    +---
    +
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: default-http-backend
    +spec:
    +  selector:
    +    matchLabels:
    +      app: default-http-backend
    +  template:
    +    metadata:
    +      labels:
    +        app: default-http-backend
    +    spec:
    +      containers:
    +      - name: default-http-backend
    +        image: gcr.io/google_containers/defaultbackend:1.3
    +
    +

    Without a separate TCP load balancer

    +

    By default, the controller will update your Ingress objects with the public IPs of the nodes running your nginx controller instances. You should run multiple instances in case of pod or node failure. The controller will do leader election and will put multiple IPs as targets in your Ingress objects in that case. It could also make sense to run it as a DaemonSet. However, we’ll just run a single replica. You have to open the respective ports on all of your worker nodes to allow nginx to receive traffic.

    +
    $ gcloud compute firewall-rules create "allow-http" --allow tcp:80 --source-ranges "0.0.0.0/0" --target-tags "gke-external-dns-9488ba14-node"
    +$ gcloud compute firewall-rules create "allow-https" --allow tcp:443 --source-ranges "0.0.0.0/0" --target-tags "gke-external-dns-9488ba14-node"
    +
    +

    Change --target-tags to the corresponding tags of your nodes. You can find them by describing your instances or by looking at the default firewall rules created by GKE for your cluster.

    +

    Apply the following manifests to your cluster to deploy the nginx-based ingress controller. Note, how it receives a reference to the default backend’s Service and that it listens on hostPorts. (You may have to use hostNetwork: true as well.)

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: nginx-ingress-controller
    +spec:
    +  selector:
    +    matchLabels:
    +      app: nginx-ingress-controller
    +  template:
    +    metadata:
    +      labels:
    +        app: nginx-ingress-controller
    +    spec:
    +      containers:
    +      - name: nginx-ingress-controller
    +        image: gcr.io/google_containers/nginx-ingress-controller:0.9.0-beta.3
    +        args:
    +        - /nginx-ingress-controller
    +        - --default-backend-service=default/default-http-backend
    +        env:
    +          - name: POD_NAME
    +            valueFrom:
    +              fieldRef:
    +                fieldPath: metadata.name
    +          - name: POD_NAMESPACE
    +            valueFrom:
    +              fieldRef:
    +                fieldPath: metadata.namespace
    +        ports:
    +        - containerPort: 80
    +          hostPort: 80
    +        - containerPort: 443
    +          hostPort: 443
    +
    +

    With a separate TCP load balancer

    +

    However, you can also have the ingress controller proxied by a Kubernetes Service. This will instruct the controller to populate this Service’s external IP as the external IP of the Ingress. This exposes the nginx proxies via a Layer 4 load balancer (type=LoadBalancer) which is more reliable than the other method. With that approach, you can run as many nginx proxy instances on your cluster as you like or have them autoscaled. This is the preferred way of running the nginx controller.

    +

    Apply the following manifests to your cluster. Note, how the controller is receiving an additional flag telling it which Service it should treat as its public endpoint and how it doesn’t need hostPorts anymore.

    +

    Apply the following manifests to run the controller in this mode.

    +
    apiVersion: v1
    +kind: Service
    +metadata:
    +  name: nginx-ingress-controller
    +spec:
    +  type: LoadBalancer
    +  ports:
    +  - name: http
    +    port: 80
    +    targetPort: 80
    +  - name: https
    +    port: 443
    +    targetPort: 443
    +  selector:
    +    app: nginx-ingress-controller
    +
    +---
    +
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: nginx-ingress-controller
    +spec:
    +  selector:
    +    matchLabels:
    +      app: nginx-ingress-controller
    +  template:
    +    metadata:
    +      labels:
    +        app: nginx-ingress-controller
    +    spec:
    +      containers:
    +      - name: nginx-ingress-controller
    +        image: gcr.io/google_containers/nginx-ingress-controller:0.9.0-beta.3
    +        args:
    +        - /nginx-ingress-controller
    +        - --default-backend-service=default/default-http-backend
    +        - --publish-service=default/nginx-ingress-controller
    +        env:
    +          - name: POD_NAME
    +            valueFrom:
    +              fieldRef:
    +                fieldPath: metadata.name
    +          - name: POD_NAMESPACE
    +            valueFrom:
    +              fieldRef:
    +                fieldPath: metadata.namespace
    +        ports:
    +        - containerPort: 80
    +        - containerPort: 443
    +
    +

    Deploy ExternalDNS

    +

    Apply the following manifest file to deploy ExternalDNS.

    +
    apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRole
    +metadata:
    +  name: external-dns
    +rules:
    +- apiGroups: [""]
    +  resources: ["services","endpoints","pods"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: ["extensions","networking.k8s.io"]
    +  resources: ["ingresses"] 
    +  verbs: ["get","watch","list"]
    +- apiGroups: [""]
    +  resources: ["nodes"]
    +  verbs: ["list"]
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRoleBinding
    +metadata:
    +  name: external-dns-viewer
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: ClusterRole
    +  name: external-dns
    +subjects:
    +- kind: ServiceAccount
    +  name: external-dns
    +  namespace: default
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      serviceAccountName: external-dns
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=ingress
    +        - --domain-filter=external-dns-test.gcp.zalan.do
    +        - --provider=google
    +        - --google-project=zalando-external-dns-test
    +        - --registry=txt
    +        - --txt-owner-id=my-identifier
    +
    +

    Use --dry-run if you want to be extra careful on the first run. Note, that you will not see any records created when you are running in dry-run mode. You can, however, inspect the logs and watch what would have been done.

    +

    Deploy a sample application

    +

    Create the following sample application to test that ExternalDNS works.

    +
    apiVersion: networking.k8s.io/v1
    +kind: Ingress
    +metadata:
    +  name: nginx
    +spec:
    +  ingressClassName: nginx
    +  rules:
    +  - host: via-ingress.external-dns-test.gcp.zalan.do
    +    http:
    +      paths:
    +      - path: /
    +        backend:
    +          service:
    +            name: nginx
    +            port:
    +              number: 80
    +        pathType: Prefix
    +
    +---
    +
    +apiVersion: v1
    +kind: Service
    +metadata:
    +  name: nginx
    +spec:
    +  ports:
    +  - port: 80
    +    targetPort: 80
    +  selector:
    +    app: nginx
    +
    +---
    +
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: nginx
    +spec:
    +  selector:
    +    matchLabels:
    +      app: nginx
    +  template:
    +    metadata:
    +      labels:
    +        app: nginx
    +    spec:
    +      containers:
    +      - image: nginx
    +        name: nginx
    +        ports:
    +        - containerPort: 80
    +
    +

    After roughly two minutes check that a corresponding DNS record for your Ingress was created.

    +
    $ gcloud dns record-sets list \
    +    --zone "external-dns-test-gcp-zalan-do" \
    +    --name "via-ingress.external-dns-test.gcp.zalan.do." \
    +    --type A
    +NAME                                         TYPE  TTL  DATA
    +via-ingress.external-dns-test.gcp.zalan.do.  A     300  35.187.1.246
    +
    +

    Let’s check that we can resolve this DNS name as well.

    +
    dig +short @ns-cloud-e1.googledomains.com. via-ingress.external-dns-test.gcp.zalan.do.
    +35.187.1.246
    +
    +

    Try with curl as well.

    +
    $ curl via-ingress.external-dns-test.gcp.zalan.do
    +<!DOCTYPE html>
    +<html>
    +<head>
    +<title>Welcome to nginx!</title>
    +...
    +</head>
    +<body>
    +...
    +</body>
    +</html>
    +
    +

    Clean up

    +

    Make sure to delete all Service and Ingress objects before terminating the cluster so all load balancers and DNS entries get cleaned up correctly.

    +
    $ kubectl delete service nginx-ingress-controller
    +$ kubectl delete ingress nginx
    +
    +

    Give ExternalDNS some time to clean up the DNS records for you. Then delete the managed zone and cluster.

    +
    $ gcloud dns managed-zones delete "external-dns-test-gcp-zalan-do"
    +$ gcloud container clusters delete "external-dns"
    +
    +

    Also delete the NS records for your removed zone from the parent zone.

    +
    $ gcloud dns record-sets transaction start --zone "gcp-zalan-do"
    +$ gcloud dns record-sets transaction remove ns-cloud-e{1..4}.googledomains.com. \
    +    --name "external-dns-test.gcp.zalan.do." --ttl 300 --type NS --zone "gcp-zalan-do"
    +$ gcloud dns record-sets transaction execute --zone "gcp-zalan-do"
    +
    +

    GKE with Workload Identity

    +

    The following instructions use GKE workload
    +identity

    +to provide ExternalDNS with the permissions it needs to manage DNS records.
    +Workload identity is the Google-recommended way to provide GKE workloads access
    +to GCP APIs.

    +

    Create a GKE cluster with workload identity enabled and without the
    +HttpLoadBalancing add-on.

    +
    $ gcloud container clusters create external-dns \
    +    --workload-metadata-from-node=GKE_METADATA_SERVER \
    +    --identity-namespace=zalando-external-dns-test.svc.id.goog \
    +    --addons=HorizontalPodAutoscaling
    +
    +

    Create a GCP service account (GSA) for ExternalDNS and save its email address.

    +
    $ sa_name="Kubernetes external-dns"
    +$ gcloud iam service-accounts create sa-edns --display-name="$sa_name"
    +$ sa_email=$(gcloud iam service-accounts list --format='value(email)' \
    +    --filter="displayName:$sa_name")
    +
    +

    Bind the ExternalDNS GSA to the DNS admin role.

    +
    $ gcloud projects add-iam-policy-binding zalando-external-dns-test \
    +    --member="serviceAccount:$sa_email" --role=roles/dns.admin
    +
    +

    Link the ExternalDNS GSA to the Kubernetes service account (KSA) that
    +external-dns will run under, i.e., the external-dns KSA in the external-dns
    +namespaces.

    +
    $ gcloud iam service-accounts add-iam-policy-binding "$sa_email" \
    +    --member="serviceAccount:zalando-external-dns-test.svc.id.goog[external-dns/external-dns]" \
    +    --role=roles/iam.workloadIdentityUser
    +
    +

    Create a DNS zone which will contain the managed DNS records.

    +
    $ gcloud dns managed-zones create external-dns-test-gcp-zalan-do \
    +    --dns-name=external-dns-test.gcp.zalan.do. \
    +    --description="Automatically managed zone by ExternalDNS"
    +
    +

    Make a note of the nameservers that were assigned to your new zone.

    +
    $ gcloud dns record-sets list \
    +    --zone=external-dns-test-gcp-zalan-do \
    +    --name=external-dns-test.gcp.zalan.do. \
    +    --type NS
    +NAME                             TYPE  TTL    DATA
    +external-dns-test.gcp.zalan.do.  NS    21600  ns-cloud-e1.googledomains.com.,ns-cloud-e2.googledomains.com.,ns-cloud-e3.googledomains.com.,ns-cloud-e4.googledomains.com.
    +
    +

    In this case it’s ns-cloud-{e1-e4}.googledomains.com. but your’s could
    +slightly differ, e.g. {a1-a4}, {b1-b4} etc.

    +

    Tell the parent zone where to find the DNS records for this zone by adding the
    +corresponding NS records there. Assuming the parent zone is “gcp-zalan-do” and
    +the domain is “gcp.zalan.do” and that it’s also hosted at Google we would do the
    +following.

    +
    $ gcloud dns record-sets transaction start --zone=gcp-zalan-do
    +$ gcloud dns record-sets transaction add ns-cloud-e{1..4}.googledomains.com. \
    +    --name=external-dns-test.gcp.zalan.do. --ttl 300 --type NS --zone=gcp-zalan-do
    +$ gcloud dns record-sets transaction execute --zone=gcp-zalan-do
    +
    +

    Connect your kubectl client to the cluster you just created and bind your GCP
    +user to the cluster admin role in Kubernetes.

    +
    $ gcloud container clusters get-credentials external-dns
    +$ kubectl create clusterrolebinding cluster-admin-me \
    +    --clusterrole=cluster-admin --user="$(gcloud config get-value account)"
    +
    +

    Deploy ingress-nginx

    +

    Follow the ingress-nginx GKE installation
    +instructions
    to
    +deploy it to the cluster.

    +
    $ kubectl apply -f \
    +    https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v0.35.0/deploy/static/provider/cloud/deploy.yaml
    +
    +

    Deploy ExternalDNS

    +

    Apply the following manifest file to deploy external-dns.

    +
    apiVersion: v1
    +kind: Namespace
    +metadata:
    +  name: external-dns
    +---
    +apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +  namespace: external-dns
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRole
    +metadata:
    +  name: external-dns
    +rules:
    +  - apiGroups: [""]
    +    resources: ["services", "endpoints", "pods"]
    +    verbs: ["get", "watch", "list"]
    +  - apiGroups: ["extensions", "networking.k8s.io"]
    +    resources: ["ingresses"]
    +    verbs: ["get", "watch", "list"]
    +  - apiGroups: [""]
    +    resources: ["nodes"]
    +    verbs: ["list"]
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRoleBinding
    +metadata:
    +  name: external-dns-viewer
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: ClusterRole
    +  name: external-dns
    +subjects:
    +  - kind: ServiceAccount
    +    name: external-dns
    +    namespace: external-dns
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +  namespace: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      containers:
    +        - args:
    +            - --source=ingress
    +            - --domain-filter=external-dns-test.gcp.zalan.do
    +            - --provider=google
    +            - --google-project=zalando-external-dns-test
    +            - --registry=txt
    +            - --txt-owner-id=my-identifier
    +          image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +          name: external-dns
    +      securityContext:
    +        fsGroup: 65534
    +        runAsUser: 65534
    +      serviceAccountName: external-dns
    +
    +

    Then add the proper workload identity annotation to the cert-manager service
    +account.

    +
    $ kubectl annotate serviceaccount --namespace=external-dns external-dns \
    +    "iam.gke.io/gcp-service-account=$sa_email"
    +
    +

    Deploy a sample application

    +

    Create the following sample application to test that ExternalDNS works.

    +
    apiVersion: networking.k8s.io/v1
    +kind: Ingress
    +metadata:
    +  name: nginx
    +spec:
    +  ingressClassName: nginx
    +  rules:
    +  - host: via-ingress.external-dns-test.gcp.zalan.do
    +    http:
    +      paths:
    +      - path: /
    +        backend:
    +          service:
    +            name: nginx
    +            port:
    +              number: 80
    +        pathType: Prefix
    +---
    +apiVersion: v1
    +kind: Service
    +metadata:
    +  name: nginx
    +spec:
    +  ports:
    +  - port: 80
    +    targetPort: 80
    +  selector:
    +    app: nginx
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: nginx
    +spec:
    +  selector:
    +    matchLabels:
    +      app: nginx
    +  template:
    +    metadata:
    +      labels:
    +        app: nginx
    +    spec:
    +      containers:
    +      - image: nginx
    +        name: nginx
    +        ports:
    +        - containerPort: 80
    +
    +

    After roughly two minutes check that a corresponding DNS record for your ingress
    +was created.

    +
    $ gcloud dns record-sets list \
    +    --zone "external-dns-test-gcp-zalan-do" \
    +    --name "via-ingress.external-dns-test.gcp.zalan.do." \
    +    --type A
    +NAME                                         TYPE  TTL  DATA
    +via-ingress.external-dns-test.gcp.zalan.do.  A     300  35.187.1.246
    +
    +

    Let’s check that we can resolve this DNS name as well.

    +
    $ dig +short @ns-cloud-e1.googledomains.com. via-ingress.external-dns-test.gcp.zalan.do.
    +35.187.1.246
    +
    +

    Try with curl as well.

    +
    $ curl via-ingress.external-dns-test.gcp.zalan.do
    +<!DOCTYPE html>
    +<html>
    +<head>
    +<title>Welcome to nginx!</title>
    +...
    +</head>
    +<body>
    +...
    +</body>
    +</html>
    +
    +

    Clean up

    +

    Make sure to delete all service and ingress objects before terminating the
    +cluster so all load balancers and DNS entries get cleaned up correctly.

    +
    $ kubectl delete service --namespace=ingress-nginx ingress-nginx-controller
    +$ kubectl delete ingress nginx
    +
    +

    Give ExternalDNS some time to clean up the DNS records for you. Then delete the
    +managed zone and cluster.

    +
    $ gcloud dns managed-zones delete external-dns-test-gcp-zalan-do
    +$ gcloud container clusters delete external-dns
    +
    +

    Also delete the NS records for your removed zone from the parent zone.

    +
    $ gcloud dns record-sets transaction start --zone gcp-zalan-do
    +$ gcloud dns record-sets transaction remove ns-cloud-e{1..4}.googledomains.com. \
    +    --name=external-dns-test.gcp.zalan.do. --ttl 300 --type NS --zone=gcp-zalan-do
    +$ gcloud dns record-sets transaction execute --zone=gcp-zalan-do
    +
    +

    User Demo How-To Blogs and Examples

    + + +
    +
    + + + Last update: + May 29, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/nodes/index.html b/v0.13.6/tutorials/nodes/index.html new file mode 100644 index 0000000000..8986e7ec45 --- /dev/null +++ b/v0.13.6/tutorials/nodes/index.html @@ -0,0 +1,2098 @@ + + + + + + + + + + + + + + + + + + Configuring ExternalDNS to use Cluster Nodes as Source - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + +
    +
    +
    + + + +
    +
    +
    + + +
    +
    + + + + + + + + +

    Configuring ExternalDNS to use Cluster Nodes as Source

    +

    This tutorial describes how to configure ExternalDNS to use the cluster nodes as source.
    +Using nodes (--source=node) as source is possible to synchronize a DNS zone with the nodes of a cluster.

    +

    The node source adds an A record per each node externalIP (if not found, any IPv4 internalIP is used instead).
    +It also adds an AAAA record per each node IPv6 internalIP.
    +The TTL of the records can be set with the external-dns.alpha.kubernetes.io/ttl node annotation.

    +

    Manifest (for cluster without RBAC enabled)

    +
    ---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      serviceAccountName: external-dns
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=node # will use nodes as source
    +        - --provider=aws
    +        - --zone-name-filter=external-dns-test.my-org.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones
    +        - --domain-filter=external-dns-test.my-org.com
    +        - --aws-zone-type=public
    +        - --registry=txt
    +        - --fqdn-template={{.Name}}.external-dns-test.my-org.com
    +        - --txt-owner-id=my-identifier
    +        - --policy=sync
    +        - --log-level=debug
    +
    +

    Manifest (for cluster with RBAC enabled)

    +
    apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRole
    +metadata:
    +  name: external-dns
    +rules:
    +- apiGroups: ["route.openshift.io"]
    +  resources: ["routes"]
    +  verbs: ["get", "watch", "list"]
    +- apiGroups: [""]
    +  resources: ["services","endpoints","pods"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: ["extensions","networking.k8s.io"]
    +  resources: ["ingresses"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: [""]
    +  resources: ["nodes"]
    +  verbs: ["get", "watch", "list"]
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRoleBinding
    +metadata:
    +  name: external-dns-viewer
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: ClusterRole
    +  name: external-dns
    +subjects:
    +- kind: ServiceAccount
    +  name: external-dns
    +  namespace: external-dns
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      serviceAccountName: external-dns
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=node # will use nodes as source
    +        - --provider=aws
    +        - --zone-name-filter=external-dns-test.my-org.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones
    +        - --domain-filter=external-dns-test.my-org.com
    +        - --aws-zone-type=public
    +        - --registry=txt
    +        - --fqdn-template={{.Name}}.external-dns-test.my-org.com
    +        - --txt-owner-id=my-identifier
    +        - --policy=sync
    +        - --log-level=debug
    +
    + +
    +
    + + + Last update: + May 29, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/ns-record/index.html b/v0.13.6/tutorials/ns-record/index.html new file mode 100644 index 0000000000..865acb43a3 --- /dev/null +++ b/v0.13.6/tutorials/ns-record/index.html @@ -0,0 +1,1949 @@ + + + + + + + + + + + + + + + + + + Creating NS record with CRD source - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + +
    +
    +
    + + + +
    +
    +
    + + +
    +
    + + + + + + + + +

    Creating NS record with CRD source

    +

    You can create NS records with the help of CRD source
    +and DNSEndpoint CRD.

    +

    Consider the following example

    +
    apiVersion: externaldns.k8s.io/v1alpha1
    +kind: DNSEndpoint
    +metadata:
    +  name: ns-record
    +spec:
    +  endpoints:
    +  - dnsName: zone.example.com
    +    recordTTL: 300
    +    recordType: NS
    +    targets:
    +    - ns1.example.com
    +    - ns2.example.com
    +
    +

    After instantiation of this Custom Resource external-dns will create NS record with the help of configured provider, e.g. aws

    + +
    +
    + + + Last update: + October 16, 2020 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/ns1/index.html b/v0.13.6/tutorials/ns1/index.html new file mode 100644 index 0000000000..4a2f3cd98d --- /dev/null +++ b/v0.13.6/tutorials/ns1/index.html @@ -0,0 +1,2314 @@ + + + + + + + + + + + + + + + + + + Setting up ExternalDNS for Services on NS1 - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + + + + +
    +
    + + + + + + + + +

    Setting up ExternalDNS for Services on NS1

    +

    This tutorial describes how to setup ExternalDNS for use within a
    +Kubernetes cluster using NS1 DNS.

    +

    Make sure to use >=0.5 version of ExternalDNS for this tutorial.

    +

    Creating a zone with NS1 DNS

    +

    If you are new to NS1, we recommend you first read the following
    +instructions for creating a zone.

    +

    Creating a zone using the NS1
    +portal

    +

    Creating a zone using the NS1
    +API

    +

    Creating NS1 Credentials

    +

    All NS1 products are API-first, meaning everything that can be done on
    +the portal—including managing zones and records, data sources and
    +feeds, and account settings and users—can be done via API.

    +

    The NS1 API is a standard REST API with JSON responses. The environment
    +var NS1_APIKEY will be needed to run ExternalDNS with NS1.

    +

    To add or delete an API key

    +
      +
    1. +

      Log into the NS1 portal at my.nsone.net.

      +
    2. +
    3. +

      Click your username in the upper-right corner, and navigate to Account Settings > Users & Teams.

      +
    4. +
    5. +

      Navigate to the API Keys tab, and click Add Key.

      +
    6. +
    7. +

      Enter the name of the application and modify permissions and settings as desired. Once complete, click Create Key. The new API key appears in the list.

      +

      Note: Set the permissions for your API keys just as you would for a user or team associated with your organization’s NS1 account. For more information, refer to the article Creating and Managing API Keys in the NS1 Knowledge Base.

      +
    8. +
    +

    Deploy ExternalDNS

    +

    Connect your kubectl client to the cluster with which you want to test ExternalDNS, and then apply one of the following manifest files for deployment:

    +

    Manifest (for clusters without RBAC enabled)

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service # ingress is also possible
    +        - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
    +        - --provider=ns1
    +        env:
    +        - name: NS1_APIKEY
    +          value: "YOUR_NS1_API_KEY"
    +
    +

    Manifest (for clusters with RBAC enabled)

    +
    apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRole
    +metadata:
    +  name: external-dns
    +rules:
    +- apiGroups: [""]
    +  resources: ["services","endpoints","pods"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: ["extensions","networking.k8s.io"]
    +  resources: ["ingresses"] 
    +  verbs: ["get","watch","list"]
    +- apiGroups: [""]
    +  resources: ["nodes"]
    +  verbs: ["list"]
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRoleBinding
    +metadata:
    +  name: external-dns-viewer
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: ClusterRole
    +  name: external-dns
    +subjects:
    +- kind: ServiceAccount
    +  name: external-dns
    +  namespace: default
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      serviceAccountName: external-dns
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service # ingress is also possible
    +        - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
    +        - --provider=ns1
    +        env:
    +        - name: NS1_APIKEY
    +          value: "YOUR_NS1_API_KEY"
    +
    +

    Deploying an Nginx Service

    +

    Create a service file called ‘nginx.yaml’ with the following contents:

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: nginx
    +spec:
    +  selector:
    +    matchLabels:
    +      app: nginx
    +  template:
    +    metadata:
    +      labels:
    +        app: nginx
    +    spec:
    +      containers:
    +      - image: nginx
    +        name: nginx
    +        ports:
    +        - containerPort: 80
    +---
    +apiVersion: v1
    +kind: Service
    +metadata:
    +  name: nginx
    +  annotations:
    +    external-dns.alpha.kubernetes.io/hostname: example.com
    +    external-dns.alpha.kubernetes.io/ttl: "120" #optional
    +spec:
    +  selector:
    +    app: nginx
    +  type: LoadBalancer
    +  ports:
    +    - protocol: TCP
    +      port: 80
    +      targetPort: 80
    +
    +

    A note about annotations

    +

    Verify that the annotation on the service uses the same hostname as the NS1 DNS zone created above. The annotation may also be a subdomain of the DNS zone (e.g. ‘www.example.com’).

    +

    The TTL annotation can be used to configure the TTL on DNS records managed by ExternalDNS and is optional. If this annotation is not set, the TTL on records managed by ExternalDNS will default to 10.

    +

    ExternalDNS uses the hostname annotation to determine which services should be registered with DNS. Removing the hostname annotation will cause ExternalDNS to remove the corresponding DNS records.

    +

    Create the deployment and service

    +
    $ kubectl create -f nginx.yaml
    +
    +

    Depending on where you run your service, it may take some time for your cloud provider to create an external IP for the service. Once an external IP is assigned, ExternalDNS detects the new service IP address and synchronizes the NS1 DNS records.

    +

    Verifying NS1 DNS records

    +

    Use the NS1 portal or API to verify that the A record for your domain shows the external IP address of the services.

    +

    Cleanup

    +

    Once you successfully configure and verify record management via ExternalDNS, you can delete the tutorial’s example:

    +
    $ kubectl delete -f nginx.yaml
    +$ kubectl delete -f externaldns.yaml
    +
    + +
    +
    + + + Last update: + May 29, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/openshift/index.html b/v0.13.6/tutorials/openshift/index.html new file mode 100644 index 0000000000..d6e6d49e84 --- /dev/null +++ b/v0.13.6/tutorials/openshift/index.html @@ -0,0 +1,2284 @@ + + + + + + + + + + + + + + + + + + Configuring ExternalDNS to use the OpenShift Route Source - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + + + + +
    +
    + + + + + + + + +

    Configuring ExternalDNS to use the OpenShift Route Source

    +

    This tutorial describes how to configure ExternalDNS to use the OpenShift Route source.
    +It is meant to supplement the other provider-specific setup tutorials.

    +

    For OCP 4.x

    +

    In OCP 4.x, if you have multiple OpenShift ingress controllers then you must specify an ingress controller name (also called router name), you can get it from the route’s status.ingress[*].routerName field.
    +If you don’t specify a router name when you have multiple ingress controllers in your cluster then the first router from the route’s status.ingress will be used. Note that the router must have admitted the route in order to be selected.
    +Once the router is known, ExternalDNS will use this router’s canonical hostname as the target for the CNAME record.

    +

    Starting from OCP 4.10 you can use ExternalDNS Operator to manage ExternalDNS instances. Example of its custom resource for AWS provider:
    +

      apiVersion: externaldns.olm.openshift.io/v1alpha1
    +  kind: ExternalDNS
    +  metadata:
    +    name: sample
    +  spec:
    +    provider:
    +      type: AWS
    +    source:
    +      openshiftRouteOptions:
    +        routerName: default
    +      type: OpenShiftRoute
    +    zones:
    +      - Z05387772BD5723IZFRX3
    +

    +

    This will create an ExternalDNS POD with the following container args in external-dns namespace:
    +

    spec:
    +  containers:
    +  - args:
    +    - --metrics-address=127.0.0.1:7979
    +    - --txt-owner-id=external-dns-sample
    +    - --provider=aws
    +    - --source=openshift-route
    +    - --policy=sync
    +    - --registry=txt
    +    - --log-level=debug
    +    - --zone-id-filter=Z05387772BD5723IZFRX3
    +    - --openshift-router-name=default
    +    - --txt-prefix=external-dns-
    +

    +

    For OCP 3.11 environment

    +

    Prepare ROUTER_CANONICAL_HOSTNAME in default/router deployment

    +

    Read and go through Finding the Host Name of the Router.
    +If no ROUTER_CANONICAL_HOSTNAME is set, you must annotate each route with external-dns.alpha.kubernetes.io/target!

    +

    Manifest (for clusters without RBAC enabled)

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=openshift-route
    +        - --domain-filter=external-dns-test.my-org.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones
    +        - --provider=aws
    +        - --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization
    +        - --aws-zone-type=public # only look at public hosted zones (valid values are public, private or no value for both)
    +        - --registry=txt
    +        - --txt-owner-id=my-identifier
    +
    +

    Manifest (for clusters with RBAC enabled)

    +
    apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRole
    +metadata:
    +  name: external-dns
    +rules:
    +- apiGroups: [""]
    +  resources: ["services","endpoints","pods"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: ["extensions","networking.k8s.io"]
    +  resources: ["ingresses"] 
    +  verbs: ["get","watch","list"]
    +- apiGroups: [""]
    +  resources: ["nodes"]
    +  verbs: ["list"]
    +- apiGroups: ["route.openshift.io"]
    +  resources: ["routes"]
    +  verbs: ["get","watch","list"]
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRoleBinding
    +metadata:
    +  name: external-dns-viewer
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: ClusterRole
    +  name: external-dns
    +subjects:
    +- kind: ServiceAccount
    +  name: external-dns
    +  namespace: default
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      serviceAccountName: external-dns
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=openshift-route
    +        - --domain-filter=external-dns-test.my-org.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones
    +        - --provider=aws
    +        - --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization
    +        - --aws-zone-type=public # only look at public hosted zones (valid values are public, private or no value for both)
    +        - --registry=txt
    +        - --txt-owner-id=my-identifier
    +
    +

    Verify External DNS works (OpenShift Route example)

    +

    The following instructions are based on the
    +Hello Openshift.

    +

    Install a sample service and expose it

    +
    $ oc apply -f - <<EOF
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  labels:
    +    app: hello-openshift
    +  name: hello-openshift
    +spec:
    +  replicas: 1
    +  selector:
    +    matchLabels:
    +      app: hello-openshift
    +  template:
    +    metadata:
    +      labels:
    +        app: hello-openshift
    +    spec:
    +      containers:
    +      - image: openshift/hello-openshift
    +        name: hello-openshift
    +---
    +apiVersion: v1
    +kind: Service
    +metadata:
    +  labels:
    +    app: hello-openshift
    +  name: hello-openshift
    +spec:
    +  ports:
    +  - port: 8080
    +    protocol: TCP
    +    targetPort: 8080
    +  selector:
    +    app: hello-openshift
    +  sessionAffinity: None
    +  type: ClusterIP
    +---
    +apiVersion: route.openshift.io/v1
    +kind: Route
    +metadata:
    +  name: hello-openshift
    +spec:
    +  host: hello-openshift.example.com
    +  to:
    +    kind: Service
    +    name: hello-openshift
    +    weight: 100
    +  wildcardPolicy: None
    +EOF
    +
    +

    Access the sample route using curl

    +
    $ curl -i http://hello-openshift.example.com
    +HTTP/1.1 200 OK
    +Date: Fri, 10 Apr 2020 09:36:41 GMT
    +Content-Length: 17
    +Content-Type: text/plain; charset=utf-8
    +
    +Hello OpenShift!
    +
    + +
    +
    + + + Last update: + May 29, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/oracle/index.html b/v0.13.6/tutorials/oracle/index.html new file mode 100644 index 0000000000..6187d882a9 --- /dev/null +++ b/v0.13.6/tutorials/oracle/index.html @@ -0,0 +1,2218 @@ + + + + + + + + + + + + + + + + + + Setting up ExternalDNS for Oracle Cloud Infrastructure (OCI) - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + +
    + +
    + + +
    +
    + + + + + + + + +

    Setting up ExternalDNS for Oracle Cloud Infrastructure (OCI)

    +

    This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using OCI DNS.

    +

    Make sure to use the latest version of ExternalDNS for this tutorial.

    +

    Creating an OCI DNS Zone

    +

    Create a DNS zone which will contain the managed DNS records. Let’s use
    +example.com as a reference here. Make note of the OCID of the compartment
    +in which you created the zone; you’ll need to provide that later.

    +

    For more information about OCI DNS see the documentation here.

    +

    Deploy ExternalDNS

    +

    Connect your kubectl client to the cluster you want to test ExternalDNS with.
    +The OCI provider supports two authentication options: key-based and instance
    +principals.

    +

    Key-based

    +

    We first need to create a config file containing the information needed to connect with the OCI API.

    +

    Create a new file (oci.yaml) and modify the contents to match the example
    +below. Be sure to adjust the values to match your own credentials, and the OCID
    +of the compartment containing the zone:

    +
    auth:
    +  region: us-phoenix-1
    +  tenancy: ocid1.tenancy.oc1...
    +  user: ocid1.user.oc1...
    +  key: |
    +    -----BEGIN RSA PRIVATE KEY-----
    +    -----END RSA PRIVATE KEY-----
    +  fingerprint: af:81:71:8e...
    +  # Omit if there is not a password for the key
    +  passphrase: Tx1jRk...
    +compartment: ocid1.compartment.oc1...
    +
    +

    Create a secret using the config file above:

    +
    $ kubectl create secret generic external-dns-config --from-file=oci.yaml
    +
    +

    OCI IAM Instance Principal

    +

    If you’re running ExternalDNS within OCI, you can use OCI IAM instance
    +principals to authenticate with OCI. This obviates the need to create the
    +secret with your credentials. You’ll need to ensure an OCI IAM policy exists
    +with a statement granting the manage dns permission on zones and records in
    +the target compartment to the dynamic group covering your instance running
    +ExternalDNS.
    +E.g.:

    +
    Allow dynamic-group <dynamic-group-name> to manage dns in compartment id <target-compartment-OCID>
    +
    +

    You’ll also need to add the --oci-auth-instance-principal flag to enable
    +this type of authentication. Finally, you’ll need to add the
    +--oci-compartment-ocid=ocid1.compartment.oc1... flag to provide the OCID of
    +the compartment containing the zone to be managed.

    +

    For more information about OCI IAM instance principals, see the documentation here.
    +For more information about OCI IAM policy details for the DNS service, see the documentation here.

    +

    Manifest (for clusters with RBAC enabled)

    +

    Apply the following manifest to deploy ExternalDNS.

    +
    apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRole
    +metadata:
    +  name: external-dns
    +rules:
    +- apiGroups: [""]
    +  resources: ["services","endpoints","pods"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: ["extensions","networking.k8s.io"]
    +  resources: ["ingresses"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: [""]
    +  resources: ["nodes"]
    +  verbs: ["list"]
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRoleBinding
    +metadata:
    +  name: external-dns-viewer
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: ClusterRole
    +  name: external-dns
    +subjects:
    +- kind: ServiceAccount
    +  name: external-dns
    +  namespace: default
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      serviceAccountName: external-dns
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service
    +        - --source=ingress
    +        - --provider=oci
    +        - --policy=upsert-only # prevent ExternalDNS from deleting any records, omit to enable full synchronization
    +        - --txt-owner-id=my-identifier
    +        volumeMounts:
    +          - name: config
    +            mountPath: /etc/kubernetes/
    +      volumes:
    +      - name: config
    +        secret:
    +          secretName: external-dns-config
    +
    +

    Verify ExternalDNS works (Service example)

    +

    Create the following sample application to test that ExternalDNS works.

    +
    +

    For services ExternalDNS will look for the annotation external-dns.alpha.kubernetes.io/hostname on the service and use the corresponding value.

    +
    +
    apiVersion: v1
    +kind: Service
    +metadata:
    +  name: nginx
    +  annotations:
    +    external-dns.alpha.kubernetes.io/hostname: example.com
    +spec:
    +  type: LoadBalancer
    +  ports:
    +  - port: 80
    +    name: http
    +    targetPort: 80
    +  selector:
    +    app: nginx
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: nginx
    +spec:
    +  selector:
    +    matchLabels:
    +      app: nginx
    +  template:
    +    metadata:
    +      labels:
    +        app: nginx
    +    spec:
    +      containers:
    +      - image: nginx
    +        name: nginx
    +        ports:
    +        - containerPort: 80
    +          name: http
    +
    +

    Apply the manifest above and wait roughly two minutes and check that a corresponding DNS record for your service was created.

    +
    $ kubectl apply -f nginx.yaml
    +
    + +
    +
    + + + Last update: + May 29, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/ovh/index.html b/v0.13.6/tutorials/ovh/index.html new file mode 100644 index 0000000000..ac5ca64ba4 --- /dev/null +++ b/v0.13.6/tutorials/ovh/index.html @@ -0,0 +1,2324 @@ + + + + + + + + + + + + + + + + + + Setting up ExternalDNS for Services on OVH - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + + + + +
    +
    + + + + + + + + +

    Setting up ExternalDNS for Services on OVH

    +

    This tutorial describes how to setup ExternalDNS for use within a
    +Kubernetes cluster using OVH DNS.

    +

    Make sure to use >=0.6 version of ExternalDNS for this tutorial.

    +

    Creating a zone with OVH DNS

    +

    If you are new to OVH, we recommend you first read the following
    +instructions for creating a zone.

    +

    Creating a zone using the OVH manager

    +

    Creating a zone using the OVH API

    +

    Creating OVH Credentials

    +

    You first need to create an OVH application.

    +

    Using the OVH documentation you will have your Application key and Application secret

    +

    And you will need to generate your consumer key, here the permissions needed :
    +- GET on /domain/zone
    +- GET on /domain/zone/*/record
    +- GET on /domain/zone/*/record/*
    +- POST on /domain/zone/*/record
    +- DELETE on /domain/zone/*/record/*
    +- GET on /domain/zone/*/soa
    +- POST on /domain/zone/*/refresh

    +

    You can use the following curl request to generate & validated your Consumer key

    +
    curl -XPOST -H "X-Ovh-Application: <ApplicationKey>" -H "Content-type: application/json" https://eu.api.ovh.com/1.0/auth/credential -d '{
    +  "accessRules": [
    +    {
    +      "method": "GET",
    +      "path": "/domain/zone"
    +    },
    +    {
    +      "method": "GET",
    +      "path": "/domain/zone/*/soa"
    +    },
    +    {
    +      "method": "GET",
    +      "path": "/domain/zone/*/record"
    +    },
    +    {
    +      "method": "GET",
    +      "path": "/domain/zone/*/record/*"
    +    },
    +    {
    +      "method": "POST",
    +      "path": "/domain/zone/*/record"
    +    },
    +    {
    +      "method": "DELETE",
    +      "path": "/domain/zone/*/record/*"
    +    },
    +    {
    +      "method": "POST",
    +      "path": "/domain/zone/*/refresh"
    +    }
    +  ],
    +  "redirection":"https://github.com/kubernetes-sigs/external-dns/blob/HEAD/docs/tutorials/ovh.md#creating-ovh-credentials"
    +}'
    +
    +

    Deploy ExternalDNS

    +

    Connect your kubectl client to the cluster with which you want to test ExternalDNS, and then apply one of the following manifest files for deployment:

    +

    Manifest (for clusters without RBAC enabled)

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service # ingress is also possible
    +        - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
    +        - --provider=ovh
    +        env:
    +        - name: OVH_APPLICATION_KEY
    +          value: "YOUR_OVH_APPLICATION_KEY"
    +        - name: OVH_APPLICATION_SECRET
    +          value: "YOUR_OVH_APPLICATION_SECRET"
    +        - name: OVH_CONSUMER_KEY
    +          value: "YOUR_OVH_CONSUMER_KEY_AFTER_VALIDATED_LINK"
    +
    +

    Manifest (for clusters with RBAC enabled)

    +
    apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRole
    +metadata:
    +  name: external-dns
    +rules:
    +- apiGroups: [""]
    +  resources: ["services"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: [""]
    +  resources: ["pods"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: ["extensions","networking.k8s.io"]
    +  resources: ["ingresses"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: [""]
    +  resources: ["nodes"]
    +  verbs: ["list"]
    +- apiGroups: [""]
    +  resources: ["endpoints"]
    +  verbs: ["get","watch","list"]
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRoleBinding
    +metadata:
    +  name: external-dns-viewer
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: ClusterRole
    +  name: external-dns
    +subjects:
    +- kind: ServiceAccount
    +  name: external-dns
    +  namespace: default
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      serviceAccountName: external-dns
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service # ingress is also possible
    +        - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
    +        - --provider=ovh
    +        env:
    +        - name: OVH_APPLICATION_KEY
    +          value: "YOUR_OVH_APPLICATION_KEY"
    +        - name: OVH_APPLICATION_SECRET
    +          value: "YOUR_OVH_APPLICATION_SECRET"
    +        - name: OVH_CONSUMER_KEY
    +          value: "YOUR_OVH_CONSUMER_KEY_AFTER_VALIDATED_LINK"
    +
    +

    Deploying an Nginx Service

    +

    Create a service file called ‘nginx.yaml’ with the following contents:

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: nginx
    +spec:
    +  selector:
    +    matchLabels:
    +      app: nginx
    +  template:
    +    metadata:
    +      labels:
    +        app: nginx
    +    spec:
    +      containers:
    +      - image: nginx
    +        name: nginx
    +        ports:
    +        - containerPort: 80
    +---
    +apiVersion: v1
    +kind: Service
    +metadata:
    +  name: nginx
    +  annotations:
    +    external-dns.alpha.kubernetes.io/hostname: example.com
    +    external-dns.alpha.kubernetes.io/ttl: "120" #optional
    +spec:
    +  selector:
    +    app: nginx
    +  type: LoadBalancer
    +  ports:
    +    - protocol: TCP
    +      port: 80
    +      targetPort: 80
    +
    +

    A note about annotations

    +

    Verify that the annotation on the service uses the same hostname as the OVH DNS zone created above. The annotation may also be a subdomain of the DNS zone (e.g. ‘www.example.com’).

    +

    The TTL annotation can be used to configure the TTL on DNS records managed by ExternalDNS and is optional. If this annotation is not set, the TTL on records managed by ExternalDNS will default to 10.

    +

    ExternalDNS uses the hostname annotation to determine which services should be registered with DNS. Removing the hostname annotation will cause ExternalDNS to remove the corresponding DNS records.

    +

    Create the deployment and service

    +
    $ kubectl create -f nginx.yaml
    +
    +

    Depending on where you run your service, it may take some time for your cloud provider to create an external IP for the service. Once an external IP is assigned, ExternalDNS detects the new service IP address and synchronizes the OVH DNS records.

    +

    Verifying OVH DNS records

    +

    Use the OVH manager or API to verify that the A record for your domain shows the external IP address of the services.

    +

    Cleanup

    +

    Once you successfully configure and verify record management via ExternalDNS, you can delete the tutorial’s example:

    +
    $ kubectl delete -f nginx.yaml
    +$ kubectl delete -f externaldns.yaml
    +
    + +
    +
    + + + Last update: + September 14, 2022 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/pdns/index.html b/v0.13.6/tutorials/pdns/index.html new file mode 100644 index 0000000000..5a38520c57 --- /dev/null +++ b/v0.13.6/tutorials/pdns/index.html @@ -0,0 +1,2217 @@ + + + + + + + + + + + + + + + + + + Setting up ExternalDNS for PowerDNS - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + +
    +
    +
    + + + +
    +
    +
    + + +
    +
    + + + + + + + + +

    Setting up ExternalDNS for PowerDNS

    +

    Prerequisites

    +

    The provider has been written for and tested against PowerDNS v4.1.x and thus requires PowerDNS Auth Server >= 4.1.x

    +

    PowerDNS provider support was added via this PR, thus you need to use external-dns version >= v0.5

    +

    The PDNS provider expects that your PowerDNS instance is already setup and
    +functional. It expects that zones, you wish to add records to, already exist
    +and are configured correctly. It does not add, remove or configure new zones in
    +anyway.

    +

    Feature Support

    +

    The PDNS provider currently does not support:

    +
      +
    • Dry running a configuration is not supported
    • +
    +

    Deployment

    +

    Deploying external DNS for PowerDNS is actually nearly identical to deploying
    +it for other providers. This is what a sample deployment.yaml looks like:

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      # Only use if you're also using RBAC
    +      # serviceAccountName: external-dns
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service # or ingress or both
    +        - --provider=pdns
    +        - --pdns-server={{ pdns-api-url }}
    +        - --pdns-api-key={{ pdns-http-api-key }}
    +        - --txt-owner-id={{ owner-id-for-this-external-dns }}
    +        - --domain-filter=external-dns-test.my-org.com # will make ExternalDNS see only the zones matching provided domain; omit to process all available zones in PowerDNS
    +        - --log-level=debug
    +        - --interval=30s
    +
    +

    Domain Filter (--domain-filter)

    +

    When the --domain-filter argument is specified, external-dns will only create DNS records for host names (specified in ingress objects and services with the external-dns annotation) related to zones 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.

    +

    The filter can also match parent zones. For example --domain-filter=a.example.com will allow for zone example.com. If you want to match parent zones, you cannot pre-pend your filter with a “.”, eg. --domain-filter=.example.com will not attempt to match parent zones.

    +

    Regex Domain Filter (--regex-domain-filter)

    +

    --regex-domain-filter limits possible domains and target zone with a regex. It overrides domain filters and can be specified only once.

    +

    RBAC

    +

    If your cluster is RBAC enabled, you also need to setup the following, before you can run external-dns:
    +

    apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRole
    +metadata:
    +  name: external-dns
    +rules:
    +- apiGroups: [""]
    +  resources: ["services","endpoints","pods"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: ["extensions","networking.k8s.io"]
    +  resources: ["ingresses"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: [""]
    +  resources: ["pods"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: [""]
    +  resources: ["nodes"]
    +  verbs: ["list"]
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRoleBinding
    +metadata:
    +  name: external-dns-viewer
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: ClusterRole
    +  name: external-dns
    +subjects:
    +- kind: ServiceAccount
    +  name: external-dns
    +  namespace: default
    +

    +

    Testing and Verification

    +

    Important!: Remember to change example.com with your own domain throughout the following text.

    +

    Spin up a simple “Hello World” HTTP server with the following spec (kubectl apply -f):

    +

    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: echo
    +spec:
    +  selector:
    +    matchLabels:
    +      app: echo
    +  template:
    +    metadata:
    +      labels:
    +        app: echo
    +    spec:
    +      containers:
    +      - image: hashicorp/http-echo
    +        name: echo
    +        ports:
    +        - containerPort: 5678
    +        args:
    +          - -text="Hello World"
    +---
    +apiVersion: v1
    +kind: Service
    +metadata:
    +  name: echo
    +  annotations:
    +    external-dns.alpha.kubernetes.io/hostname: echo.example.com
    +spec:
    +  selector:
    +    app: echo
    +  type: LoadBalancer
    +  ports:
    +    - protocol: TCP
    +      port: 80
    +      targetPort: 5678
    +

    +Important!: Don’t run dig, nslookup or similar immediately (until you’ve
    +confirmed the record exists). You’ll get hit by negative DNS caching, which is hard to flush.

    +

    Run the following to make sure everything is in order:

    +
    $ kubectl get services echo
    +$ kubectl get endpoints echo
    +
    +

    Make sure everything looks correct, i.e the service is defined and receives a
    +public IP, and that the endpoint also has a pod IP.

    +

    Once that’s done, wait about 30s-1m (interval for external-dns to kick in), then do:
    +

    $ curl -H "X-API-Key: ${PDNS_API_KEY}" ${PDNS_API_URL}/api/v1/servers/localhost/zones/example.com. | jq '.rrsets[] | select(.name | contains("echo"))'
    +

    +

    Once the API shows the record correctly, you can double check your record using:
    +

    $ dig @${PDNS_FQDN} echo.example.com.
    +

    + +
    +
    + + + Last update: + July 31, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/pihole/index.html b/v0.13.6/tutorials/pihole/index.html new file mode 100644 index 0000000000..493cee8a87 --- /dev/null +++ b/v0.13.6/tutorials/pihole/index.html @@ -0,0 +1,2234 @@ + + + + + + + + + + + + + + + + + + Setting up ExternalDNS for Pi-hole - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + +
    +
    +
    + + + +
    +
    +
    + + +
    +
    + + + + + + + + +

    Setting up ExternalDNS for Pi-hole

    +

    This tutorial describes how to setup ExternalDNS to sync records with Pi-hole’s Custom DNS.
    +Pi-hole has an internal list it checks last when resolving requests. This list can contain any number of arbitrary A or CNAME records.
    +There is a pseudo-API exposed that ExternalDNS is able to use to manage these records.

    +

    Deploy ExternalDNS

    +

    You can skip to the manifest if authentication is disabled on your Pi-hole instance or you don’t want to use secrets.

    +

    If your Pi-hole server’s admin dashboard is protected by a password, you’ll likely want to create a secret first containing its value.
    +This is optional since you do retain the option to pass it as a flag with --pihole-password.

    +

    You can create the secret with:

    +
    kubectl create secret generic pihole-password \
    +    --from-literal EXTERNAL_DNS_PIHOLE_PASSWORD=supersecret
    +
    +

    Replacing “supersecret” with the actual password to your Pi-hole server.

    +

    ExternalDNS Manifest

    +

    Apply the following manifest to deploy ExternalDNS, editing values for your environment accordingly.
    +Be sure to change the namespace in the ClusterRoleBinding if you are using a namespace other than default.

    +
    ---
    +apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRole
    +metadata:
    +  name: external-dns
    +rules:
    +- apiGroups: [""]
    +  resources: ["services","endpoints","pods"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: ["extensions","networking.k8s.io"]
    +  resources: ["ingresses"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: [""]
    +  resources: ["nodes"]
    +  verbs: ["list","watch"]
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRoleBinding
    +metadata:
    +  name: external-dns-viewer
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: ClusterRole
    +  name: external-dns
    +subjects:
    +- kind: ServiceAccount
    +  name: external-dns
    +  namespace: default
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      serviceAccountName: external-dns
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        # If authentication is disabled and/or you didn't create
    +        # a secret, you can remove this block.
    +        envFrom:
    +        - secretRef:
    +            # Change this if you gave the secret a different name
    +            name: pihole-password
    +        args:
    +        - --source=service
    +        - --source=ingress
    +        # Pihole only supports A/CNAME records so there is no mechanism to track ownership.
    +        # You don't need to set this flag, but if you leave it unset, you will receive warning
    +        # logs when ExternalDNS attempts to create TXT records.
    +        - --registry=noop
    +        # IMPORTANT: If you have records that you manage manually in Pi-hole, set
    +        # the policy to upsert-only so they do not get deleted.
    +        - --policy=upsert-only
    +        - --provider=pihole
    +        # Change this to the actual address of your Pi-hole web server
    +        - --pihole-server=http://pihole-web.pihole.svc.cluster.local
    +      securityContext:
    +        fsGroup: 65534 # For ExternalDNS to be able to read Kubernetes token files
    +
    +

    Arguments

    +
      +
    • --pihole-server (env: EXTERNAL_DNS_PIHOLE_SERVER) - The address of the Pi-hole web server
    • +
    • --pihole-password (env: EXTERNAL_DNS_PIHOLE_PASSWORD) - The password to the Pi-hole web server (if enabled)
    • +
    • --pihole-tls-skip-verify (env: EXTERNAL_DNS_PIHOLE_TLS_SKIP_VERIFY) - Skip verification of any TLS certificates served by the Pi-hole web server.
    • +
    +

    Verify ExternalDNS Works

    +

    Ingress Example

    +

    Create an Ingress resource. ExternalDNS will use the hostname specified in the Ingress object.

    +
    apiVersion: networking.k8s.io/v1
    +kind: Ingress
    +metadata:
    +  name: foo
    +spec:
    +  ingressClassName: nginx
    +  rules:
    +  - host: foo.bar.com
    +    http:
    +      paths:
    +      - path: /
    +        pathType: Prefix
    +        backend:
    +          service:
    +            name: foo
    +            port:
    +              number: 80
    +
    +

    Service Example

    +

    The below sample application can be used to verify Services work.
    +For services ExternalDNS will look for the annotation external-dns.alpha.kubernetes.io/hostname on the service and use the corresponding value.

    +
    ---
    +apiVersion: v1
    +kind: Service
    +metadata:
    +  name: nginx
    +  annotations:
    +    external-dns.alpha.kubernetes.io/hostname: nginx.external-dns-test.homelab.com
    +spec:
    +  type: LoadBalancer
    +  ports:
    +  - port: 80
    +    name: http
    +    targetPort: 80
    +  selector:
    +    app: nginx
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: nginx
    +spec:
    +  selector:
    +    matchLabels:
    +      app: nginx
    +  template:
    +    metadata:
    +      labels:
    +        app: nginx
    +    spec:
    +      containers:
    +      - image: nginx
    +        name: nginx
    +        ports:
    +        - containerPort: 80
    +          name: http
    +
    +

    You can then query your Pi-hole to see if the record was created.

    +

    Change @192.168.100.2 to the actual address of your DNS server

    +
    $ dig +short @192.168.100.2  nginx.external-dns-test.homelab.com
    +
    +192.168.100.129
    +
    + +
    +
    + + + Last update: + May 29, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/plural/index.html b/v0.13.6/tutorials/plural/index.html new file mode 100644 index 0000000000..be9f6c66f1 --- /dev/null +++ b/v0.13.6/tutorials/plural/index.html @@ -0,0 +1,2239 @@ + + + + + + + + + + + + + + + + + + Setting up ExternalDNS for Services on Plural - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + + + + +
    +
    + + + + + + + + +

    Setting up ExternalDNS for Services on Plural

    +

    This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using Plural DNS.

    +

    Make sure to use >=0.12.3 version of ExternalDNS for this tutorial.

    +

    Creating Plural Credentials

    +

    A secret containing the a Plural access token is needed for this provider. You can get a token for your user here.

    +

    To create the secret you can run kubectl create secret generic plural-env --from-literal=PLURAL_ACCESS_TOKEN=<replace-with-your-access-token>.

    +

    Deploy ExternalDNS

    +

    Connect your kubectl client to the cluster you want to test ExternalDNS with.
    +Then apply one of the following manifests file to deploy ExternalDNS.

    +

    Manifest (for clusters without RBAC enabled)

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service # ingress is also possible
    +        - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
    +        - --provider=plural
    +        - --plural-cluster=example-plural-cluster
    +        - --plural-provider=aws # gcp, azure, equinix and kind are also possible
    +        env:
    +        - name: PLURAL_ACCESS_TOKEN
    +          valueFrom:
    +            secretKeyRef:
    +              key: PLURAL_ACCESS_TOKEN
    +              name: plural-env
    +        - name: PLURAL_ENDPOINT # (optional) use an alternative endpoint for Plural; defaults to https://app.plural.sh
    +          value: https://app.plural.sh
    +
    +

    Manifest (for clusters with RBAC enabled)

    +
    apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRole
    +metadata:
    +  name: external-dns
    +rules:
    +- apiGroups: [""]
    +  resources: ["services","endpoints","pods"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: ["extensions","networking.k8s.io"]
    +  resources: ["ingresses"] 
    +  verbs: ["get","watch","list"]
    +- apiGroups: [""]
    +  resources: ["nodes"]
    +  verbs: ["list", "watch"]
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRoleBinding
    +metadata:
    +  name: external-dns-viewer
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: ClusterRole
    +  name: external-dns
    +subjects:
    +- kind: ServiceAccount
    +  name: external-dns
    +  namespace: default
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service # ingress is also possible
    +        - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
    +        - --provider=plural
    +        - --plural-cluster=example-plural-cluster
    +        - --plural-provider=aws # gcp, azure, equinix and kind are also possible
    +        env:
    +        - name: PLURAL_ACCESS_TOKEN
    +          valueFrom:
    +            secretKeyRef:
    +              key: PLURAL_ACCESS_TOKEN
    +              name: plural-env
    +        - name: PLURAL_ENDPOINT # (optional) use an alternative endpoint for Plural; defaults to https://app.plural.sh
    +          value: https://app.plural.sh
    +
    +

    Deploying an Nginx Service

    +

    Create a service file called ‘nginx.yaml’ with the following contents:

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: nginx
    +spec:
    +  selector:
    +    matchLabels:
    +      app: nginx
    +  template:
    +    metadata:
    +      labels:
    +        app: nginx
    +    spec:
    +      containers:
    +      - image: nginx
    +        name: nginx
    +        ports:
    +        - containerPort: 80
    +---
    +apiVersion: v1
    +kind: Service
    +metadata:
    +  name: nginx
    +  annotations:
    +    external-dns.alpha.kubernetes.io/hostname: example.com
    +spec:
    +  selector:
    +    app: nginx
    +  type: LoadBalancer
    +  ports:
    +    - protocol: TCP
    +      port: 80
    +      targetPort: 80
    +
    +

    Note the annotation on the service; use the same hostname as the Plural DNS zone created above. The annotation may also be a subdomain
    +of the DNS zone (e.g. ‘www.example.com’).

    +

    By setting the TTL annotation on the service, you have to pass a valid TTL, which must be 120 or above.
    +This annotation is optional, if you won’t set it, it will be 1 (automatic) which is 300.

    +

    ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation
    +will cause ExternalDNS to remove the corresponding DNS records.

    +

    Create the deployment and service:

    +
    $ kubectl create -f nginx.yaml
    +
    +

    Depending where you run your service it can take a little while for your cloud provider to create an external IP for the service.

    +

    Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize
    +the Plural DNS records.

    +

    Verifying Plural DNS records

    +

    Check your Plural domain overview to view the domains associated with your Plural account. There you can view the records for each domain.

    +

    The records should show the external IP address of the service as the A record for your domain.

    +

    Cleanup

    +

    Now that we have verified that ExternalDNS will automatically manage Plural DNS records, we can delete the tutorial’s example:

    +

    ```
    +$ kubectl delete -f nginx.yaml
    +$ kubectl delete -f externaldns.yaml

    + +
    +
    + + + Last update: + May 29, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/public-private-route53/index.html b/v0.13.6/tutorials/public-private-route53/index.html new file mode 100644 index 0000000000..08c7ad3a78 --- /dev/null +++ b/v0.13.6/tutorials/public-private-route53/index.html @@ -0,0 +1,2387 @@ + + + + + + + + + + + + + + + + + + Setting up ExternalDNS using the same domain for public and private Route53 zones - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + + + + +
    +
    + + + + + + + + +

    Setting up ExternalDNS using the same domain for public and private Route53 zones

    +

    This tutorial describes how to setup ExternalDNS using the same domain for public and private Route53 zones and nginx-ingress-controller. It also outlines how to use cert-manager to automatically issue SSL certificates from Let’s Encrypt for both public and private records.

    +

    Deploy public nginx-ingress-controller

    +

    Consult External DNS nginx ingress docs for installation guidelines.

    +

    Specify ingress-class in nginx-ingress-controller container args:

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  labels:
    +    app: external-ingress
    +  name: external-ingress-controller
    +spec:
    +  replicas: 1
    +  selector:
    +    matchLabels:
    +      app: external-ingress
    +  template:
    +    metadata:
    +      labels:
    +        app: external-ingress
    +    spec:
    +      containers:
    +      - args:
    +        - /nginx-ingress-controller
    +        - --default-backend-service=$(POD_NAMESPACE)/default-http-backend
    +        - --configmap=$(POD_NAMESPACE)/external-ingress-configuration
    +        - --tcp-services-configmap=$(POD_NAMESPACE)/external-tcp-services
    +        - --udp-services-configmap=$(POD_NAMESPACE)/external-udp-services
    +        - --annotations-prefix=nginx.ingress.kubernetes.io
    +        - --ingress-class=external-ingress
    +        - --publish-service=$(POD_NAMESPACE)/external-ingress
    +        env:
    +        - name: POD_NAME
    +          valueFrom:
    +            fieldRef:
    +              apiVersion: v1
    +              fieldPath: metadata.name
    +        - name: POD_NAMESPACE
    +          valueFrom:
    +            fieldRef:
    +              apiVersion: v1
    +              fieldPath: metadata.namespace
    +        image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.11.0
    +        livenessProbe:
    +          failureThreshold: 3
    +          httpGet:
    +            path: /healthz
    +            port: 10254
    +            scheme: HTTP
    +          initialDelaySeconds: 10
    +          periodSeconds: 10
    +          successThreshold: 1
    +          timeoutSeconds: 1
    +        name: external-ingress-controller
    +        ports:
    +        - containerPort: 80
    +          name: http
    +          protocol: TCP
    +        - containerPort: 443
    +          name: https
    +          protocol: TCP
    +        readinessProbe:
    +          failureThreshold: 3
    +          httpGet:
    +            path: /healthz
    +            port: 10254
    +            scheme: HTTP
    +          periodSeconds: 10
    +          successThreshold: 1
    +          timeoutSeconds: 1
    +
    +

    Set type: LoadBalancer in your public nginx-ingress-controller Service definition.

    +
    apiVersion: v1
    +kind: Service
    +metadata:
    +  annotations:
    +    service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout: "3600"
    +    service.beta.kubernetes.io/aws-load-balancer-proxy-protocol: '*'
    +  labels:
    +    app: external-ingress
    +  name: external-ingress
    +spec:
    +  externalTrafficPolicy: Cluster
    +  ports:
    +  - name: http
    +    port: 80
    +    protocol: TCP
    +    targetPort: http
    +  - name: https
    +    port: 443
    +    protocol: TCP
    +    targetPort: https
    +  selector:
    +    app: external-ingress
    +  sessionAffinity: None
    +  type: LoadBalancer
    +
    +

    Deploy private nginx-ingress-controller

    +

    Consult External DNS nginx ingress docs for installation guidelines.

    +

    Make sure to specify ingress-class in nginx-ingress-controller container args:

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  labels:
    +    app: internal-ingress
    +  name: internal-ingress-controller
    +spec:
    +  replicas: 1
    +  selector:
    +    matchLabels:
    +      app: internal-ingress
    +  template:
    +    metadata:
    +      labels:
    +        app: internal-ingress
    +    spec:
    +      containers:
    +      - args:
    +        - /nginx-ingress-controller
    +        - --default-backend-service=$(POD_NAMESPACE)/default-http-backend
    +        - --configmap=$(POD_NAMESPACE)/internal-ingress-configuration
    +        - --tcp-services-configmap=$(POD_NAMESPACE)/internal-tcp-services
    +        - --udp-services-configmap=$(POD_NAMESPACE)/internal-udp-services
    +        - --annotations-prefix=nginx.ingress.kubernetes.io
    +        - --ingress-class=internal-ingress
    +        - --publish-service=$(POD_NAMESPACE)/internal-ingress
    +        env:
    +        - name: POD_NAME
    +          valueFrom:
    +            fieldRef:
    +              apiVersion: v1
    +              fieldPath: metadata.name
    +        - name: POD_NAMESPACE
    +          valueFrom:
    +            fieldRef:
    +              apiVersion: v1
    +              fieldPath: metadata.namespace
    +        image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.11.0
    +        livenessProbe:
    +          failureThreshold: 3
    +          httpGet:
    +            path: /healthz
    +            port: 10254
    +            scheme: HTTP
    +          initialDelaySeconds: 10
    +          periodSeconds: 10
    +          successThreshold: 1
    +          timeoutSeconds: 1
    +        name: internal-ingress-controller
    +        ports:
    +        - containerPort: 80
    +          name: http
    +          protocol: TCP
    +        - containerPort: 443
    +          name: https
    +          protocol: TCP
    +        readinessProbe:
    +          failureThreshold: 3
    +          httpGet:
    +            path: /healthz
    +            port: 10254
    +            scheme: HTTP
    +          periodSeconds: 10
    +          successThreshold: 1
    +          timeoutSeconds: 1
    +
    +

    Set additional annotations in your private nginx-ingress-controller Service definition to create an internal load balancer.

    +
    apiVersion: v1
    +kind: Service
    +metadata:
    +  annotations:
    +    service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout: "3600"
    +    service.beta.kubernetes.io/aws-load-balancer-internal: 0.0.0.0/0
    +    service.beta.kubernetes.io/aws-load-balancer-proxy-protocol: '*'
    +  labels:
    +    app: internal-ingress
    +  name: internal-ingress
    +spec:
    +  externalTrafficPolicy: Cluster
    +  ports:
    +  - name: http
    +    port: 80
    +    protocol: TCP
    +    targetPort: http
    +  - name: https
    +    port: 443
    +    protocol: TCP
    +    targetPort: https
    +  selector:
    +    app: internal-ingress
    +  sessionAffinity: None
    +  type: LoadBalancer
    +
    +

    Deploy the public zone ExternalDNS

    +

    Consult AWS ExternalDNS setup docs for installation guidelines.

    +

    In ExternalDNS containers args, make sure to specify aws-zone-type and ingress-class:

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  labels:
    +    app: external-dns-public
    +  name: external-dns-public
    +  namespace: kube-system
    +spec:
    +  replicas: 1
    +  selector:
    +    matchLabels:
    +      app: external-dns-public
    +  strategy:
    +    type: Recreate
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns-public
    +    spec:
    +      containers:
    +      - args:
    +        - --source=ingress
    +        - --provider=aws
    +        - --registry=txt
    +        - --txt-owner-id=external-dns
    +        - --ingress-class=external-ingress
    +        - --aws-zone-type=public
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        name: external-dns-public
    +
    +

    Deploy the private zone ExternalDNS

    +

    Consult AWS ExternalDNS setup docs for installation guidelines.

    +

    In ExternalDNS containers args, make sure to specify aws-zone-type and ingress-class:

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  labels:
    +    app: external-dns-private
    +  name: external-dns-private
    +  namespace: kube-system
    +spec:
    +  replicas: 1
    +  selector:
    +    matchLabels:
    +      app: external-dns-private
    +  strategy:
    +    type: Recreate
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns-private
    +    spec:
    +      containers:
    +      - args:
    +        - --source=ingress
    +        - --provider=aws
    +        - --registry=txt
    +        - --txt-owner-id=dev.k8s.nexus
    +        - --ingress-class=internal-ingress
    +        - --aws-zone-type=private
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        name: external-dns-private
    +
    +

    Create application Service definitions

    +

    For this setup to work, you need to create two Ingress definitions for your application.

    +

    At first, create a public Ingress definition:

    +
    apiVersion: networking.k8s.io/v1
    +kind: Ingress
    +metadata:
    +  labels:
    +    app: app
    +  name: app-public
    +spec:
    +  ingressClassName: external-ingress
    +  rules:
    +  - host: app.domain.com
    +    http:
    +      paths:
    +      - backend:
    +          service:
    +            name: app
    +            port:
    +              number: 80
    +        pathType: Prefix
    +
    +

    Then create a private Ingress definition:

    +
    apiVersion: networking.k8s.io/v1
    +kind: Ingress
    +metadata:
    +  labels:
    +    app: app
    +  name: app-private
    +spec:
    +  ingressClassName: internal-ingress
    +  rules:
    +  - host: app.domain.com
    +    http:
    +      paths:
    +      - backend:
    +          service:
    +            name: app
    +            port:
    +              number: 80
    +        pathType: Prefix
    +
    +

    Additionally, you may leverage cert-manager to automatically issue SSL certificates from Let’s Encrypt. To do that, request a certificate in public service definition:

    +
    apiVersion: networking.k8s.io/v1
    +kind: Ingress
    +metadata:
    +  annotations:
    +    certmanager.k8s.io/acme-challenge-type: "dns01"
    +    certmanager.k8s.io/acme-dns01-provider: "route53"
    +    certmanager.k8s.io/cluster-issuer: "letsencrypt-production"
    +    kubernetes.io/tls-acme: "true"
    +  labels:
    +    app: app
    +  name: app-public
    +spec:
    +  ingressClassName: "external-ingress"
    +  rules:
    +  - host: app.domain.com
    +    http:
    +      paths:
    +      - backend:
    +          service:
    +            name: app
    +            port:
    +              number: 80
    +        pathType: Prefix
    +  tls:
    +  - hosts:
    +    - app.domain.com
    +    secretName: app-tls
    +
    +

    And reuse the requested certificate in private Service definition:

    +
    apiVersion: networking.k8s.io/v1
    +kind: Ingress
    +metadata:
    +  labels:
    +    app: app
    +  name: app-private
    +spec:
    +  ingressClassName: "internal-ingress"
    +  rules:
    +  - host: app.domain.com
    +    http:
    +      paths:
    +      - backend:
    +          service:
    +            name: app
    +            port:
    +              number: 80
    +        pathType: Prefix
    +  tls:
    +  - hosts:
    +    - app.domain.com
    +    secretName: app-tls
    +
    + +
    +
    + + + Last update: + May 31, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/rcodezero/index.html b/v0.13.6/tutorials/rcodezero/index.html new file mode 100644 index 0000000000..a332846d48 --- /dev/null +++ b/v0.13.6/tutorials/rcodezero/index.html @@ -0,0 +1,2266 @@ + + + + + + + + + + + + + + + + + + Setting up ExternalDNS for Services on RcodeZero - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + + + + +
    +
    + + + + + + + + +

    Setting up ExternalDNS for Services on RcodeZero

    +

    This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using RcodeZero Anycast DNS. Make sure to use >=0.5.0 version of ExternalDNS for this tutorial.

    +

    The following steps are required to use RcodeZero with ExternalDNS:

    +
      +
    1. Sign up for an RcodeZero account (or use an existing account).
    2. +
    3. Add your zone to the RcodeZero DNS
    4. +
    5. Enable the RcodeZero API, and generate an API key.
    6. +
    7. Deploy ExternalDNS to use the RcodeZero provider.
    8. +
    9. Verify the setup bey deploying a test services (optional)
    10. +
    +

    Creating a RcodeZero DNS zone

    +

    Before records can be added to your domain name automatically, you need to add your domain name to the set of zones managed by RcodeZero. In order to add the zone, perform the following steps:

    +
      +
    1. Log in to the RcodeZero Dashboard, and move to the Add Zone page.
    2. +
    3. Select “MASTER” as domain type, and add your domain name there. Use this domain name instead of “example.com” throughout the rest of this tutorial.
    4. +
    +

    Note that “SECONDARY” domains cannot be managed by ExternalDNS, because this would not allow modification of records in the zone.

    +

    Enable the API, and create Credentials

    +
    +

    The RcodeZero Anycast-Network is provisioned via web interface or REST-API.

    +
    +

    Enable the RcodeZero API to generate an API key on RcodeZero API. The API key will be added to the environment variable ‘RC0_API_KEY’ via one of the Manifest templates (as described below).

    +

    Deploy ExternalDNS

    +

    Connect your kubectl client to the cluster you want to test ExternalDNS with. Choose a Manifest from below, depending on whether or not you have RBAC enabled. Before applying it, modify the Manifest as follows:

    +
      +
    • Replace “example.com” with the domain name you added to RcodeZero.
    • +
    • Replace YOUR_RCODEZERO_API_KEY with the API key created above.
    • +
    • Replace YOUR_ENCRYPTION_KEY_STRING with a string to encrypt the TXT records
    • +
    +

    Manifest (for clusters without RBAC enabled)

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service # ingress is also possible
    +        - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
    +        - --provider=rcodezero
    +        - --rc0-enc-txt # (optional) encrypt TXT records; encryption key has to be provided with RC0_ENC_KEY env var.
    +        env:
    +        - name: RC0_API_KEY
    +          value: "YOUR_RCODEZERO_API_KEY"
    +        - name: RC0_ENC_VAR
    +          value: "YOUR_ENCRYPTION_KEY_STRING"
    +
    +

    Manifest (for clusters with RBAC enabled)

    +
    apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRole
    +metadata:
    +  name: external-dns
    +rules:
    +- apiGroups: [""]
    +  resources: ["services","endpoints","pods"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: ["extensions","networking.k8s.io"]
    +  resources: ["ingresses"] 
    +  verbs: ["get","watch","list"]
    +- apiGroups: [""]
    +  resources: ["nodes"]
    +  verbs: ["list"]
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRoleBinding
    +metadata:
    +  name: external-dns-viewer
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: ClusterRole
    +  name: external-dns
    +subjects:
    +- kind: ServiceAccount
    +  name: external-dns
    +  namespace: default
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      serviceAccountName: external-dns
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service # ingress is also possible
    +        - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
    +        - --provider=rcodezero
    +        - --rc0-enc-txt # (optional) encrypt TXT records; encryption key has to be provided with RC0_ENC_KEY env var.
    +        env:
    +        - name: RC0_API_KEY
    +          value: "YOUR_RCODEZERO_API_KEY"
    +        - name: RC0_ENC_VAR
    +          value: "YOUR_ENCRYPTION_KEY_STRING"
    +
    +

    Deploying an Nginx Service

    +

    After you have deployed ExternalDNS with RcodeZero, you can deploy a simple service based on Nginx to test the setup. This is optional, though highly recommended before using ExternalDNS in production.

    +

    Create a service file called ‘nginx.yaml’ with the following contents:

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: nginx
    +spec:
    +  selector:
    +    matchLabels:
    +      app: nginx
    +  template:
    +    metadata:
    +      labels:
    +        app: nginx
    +    spec:
    +      containers:
    +      - image: nginx
    +        name: nginx
    +        ports:
    +        - containerPort: 80
    +---
    +apiVersion: v1
    +kind: Service
    +metadata:
    +  name: nginx
    +  annotations:
    +    external-dns.alpha.kubernetes.io/hostname: example.com
    +    external-dns.alpha.kubernetes.io/ttl: "120" #optional
    +spec:
    +  selector:
    +    app: nginx
    +  type: LoadBalancer
    +  ports:
    +    - protocol: TCP
    +      port: 80
    +      targetPort: 80
    +
    +

    Change the file as follows:

    +
      +
    • Replace the annotation of the service; use the same hostname as the RcodeZero DNS zone created above. The annotation may also be a subdomain
      +of the DNS zone (e.g. ‘www.example.com’).
    • +
    • Set the TTL annotation of the service. A valid TTL of 120 or above must be given. This annotation is optional, and defaults to “300” if no value is given.
    • +
    +

    These annotations will be used to determine what services should be registered with DNS. Removing these annotations will cause ExternalDNS to remove the corresponding DNS records.

    +

    Create the Deployment and Service:

    +
    $ kubectl create -f nginx.yaml
    +
    +

    Depending on your cloud provider, it might take a while to create an external IP for the service. Once an external IP address is assigned to the service, ExternalDNS will notice the new address and synchronize the RcodeZero DNS records accordingly.

    +

    Verifying RcodeZero DNS records

    +

    Check your RcodeZero Configured Zones and select the respective zone name. The zone should now contain the external IP address of the service as an A record.

    +

    Cleanup

    +

    Once you have verified that ExternalDNS successfully manages RcodeZero DNS records for external services, you can delete the tutorial example as follows:

    +
    $ kubectl delete -f nginx.yaml
    +$ kubectl delete -f externaldns.yaml
    +
    + +
    +
    + + + Last update: + May 29, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/rdns/index.html b/v0.13.6/tutorials/rdns/index.html new file mode 100644 index 0000000000..b21f9c8ab4 --- /dev/null +++ b/v0.13.6/tutorials/rdns/index.html @@ -0,0 +1,2280 @@ + + + + + + + + + + + + + + + + + + Setting up ExternalDNS for RancherDNS(RDNS) with kubernetes - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + + + + +
    +
    + + + + + + + + +

    Setting up ExternalDNS for RancherDNS(RDNS) with kubernetes

    +

    This tutorial describes how to setup ExternalDNS for usage within a kubernetes cluster that makes use of RDNS and nginx ingress controller.
    +You need to:
    +* install RDNS with etcd enabled
    +* install external-dns with rdns as a provider

    +

    Installing RDNS with etcdv3 backend

    +

    Clone RDNS

    +
    git clone https://github.com/rancher/rdns-server.git
    +
    +

    Installing ETCD

    +
    cd rdns-server
    +docker-compose -f deploy/etcdv3/etcd-compose.yaml up -d
    +
    +
    +

    ETCD was successfully deployed on http://172.31.35.77:2379

    +
    +

    Installing RDNS

    +
    export ETCD_ENDPOINTS="http://172.31.35.77:2379"
    +export DOMAIN="lb.rancher.cloud"
    +./scripts/start etcdv3
    +
    +
    +

    RDNS was successfully deployed on 172.31.35.77

    +
    +

    Installing ExternalDNS

    +

    Install external ExternalDNS

    +

    ETCD_URLS is configured to etcd client service address.
    +RDNS_ROOT_DOMAIN is configured to the same with RDNS DOMAIN environment. e.g. lb.rancher.cloud.

    +

    Manifest (for clusters without RBAC enabled)

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +  namespace: kube-system
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      serviceAccountName: external-dns
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=ingress
    +        - --provider=rdns
    +        - --log-level=debug # debug only
    +        env:
    +        - name: ETCD_URLS
    +          value: http://172.31.35.77:2379
    +        - name: RDNS_ROOT_DOMAIN
    +          value: lb.rancher.cloud
    +
    +

    Manifest (for clusters with RBAC enabled)

    +
    ---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRole
    +metadata:
    +  name: external-dns
    +rules:
    +- apiGroups: [""]
    +  resources: ["services","endpoints","pods"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: ["extensions","networking.k8s.io"]
    +  resources: ["ingresses"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: [""]
    +  resources: ["nodes"]
    +  verbs: ["list"]
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRoleBinding
    +metadata:
    +  name: external-dns-viewer
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: ClusterRole
    +  name: external-dns
    +subjects:
    +- kind: ServiceAccount
    +  name: external-dns
    +  namespace: kube-system
    +---
    +apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +  namespace: kube-system
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +  namespace: kube-system
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      serviceAccountName: external-dns
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=ingress
    +        - --provider=rdns
    +        - --log-level=debug # debug only
    +        env:
    +        - name: ETCD_URLS
    +          value: http://172.31.35.77:2379
    +        - name: RDNS_ROOT_DOMAIN
    +          value: lb.rancher.cloud
    +
    +

    Testing ingress example

    +
    $ cat ingress.yaml
    +apiVersion: networking.k8s.io/v1
    +kind: Ingress
    +metadata:
    +  name: nginx
    +spec:
    +  ingressClassName: nginx
    +  rules:
    +  - host: nginx.lb.rancher.cloud
    +    http:
    +      paths:
    +      - backend:
    +          serviceName: nginx
    +          servicePort: 80
    +
    +$ kubectl apply -f ingress.yaml
    +ingress.extensions "nginx" created
    +
    +

    Wait a moment until DNS has the ingress IP. The RDNS IP in this example is “172.31.35.77”.
    +

    $ kubectl get ingress
    +NAME      HOSTS                    ADDRESS         PORTS     AGE
    +nginx     nginx.lb.rancher.cloud   172.31.42.211   80        2m
    +
    +$ kubectl run -it --rm --restart=Never --image=infoblox/dnstools:latest dnstools
    +If you don't see a command prompt, try pressing enter.
    +dnstools# dig @172.31.35.77 nginx.lb.rancher.cloud +short
    +172.31.42.211
    +dnstools#  
    +

    + +
    +
    + + + Last update: + May 29, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/rfc2136/index.html b/v0.13.6/tutorials/rfc2136/index.html new file mode 100644 index 0000000000..7a2e076b35 --- /dev/null +++ b/v0.13.6/tutorials/rfc2136/index.html @@ -0,0 +1,2586 @@ + + + + + + + + + + + + + + + + + + Configuring RFC2136 provider - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + + + + +
    +
    + + + + + + + + +

    Configuring RFC2136 provider

    +

    This tutorial describes how to use the RFC2136 with either BIND or Windows DNS.

    +

    Using with BIND

    +

    To use external-dns with BIND: generate/procure a key, configure DNS and add a
    +deployment of external-dns.

    +

    Server credentials:

    +
      +
    • RFC2136 was developed for and tested with
      +BIND DNS server. This documentation
      +assumes that you already have a configured and working server. If you don’t,
      +please check BIND documents or tutorials.
    • +
    • If your DNS is provided for you, ask for a TSIG key authorized to update and
      +transfer the zone you wish to update. The key will look something like below.
      +Skip the next steps wrt BIND setup.
      +
      key "externaldns-key" {
      +    algorithm hmac-sha256;
      +    secret "96Ah/a2g0/nLeFGK+d/0tzQcccf9hCEIy34PoXX2Qg8=";
      +};
      +
    • +
    • If you are your own DNS administrator create a TSIG key. Use
      +tsig-keygen -a hmac-sha256 externaldns or on older distributions
      +dnssec-keygen -a HMAC-SHA256 -b 256 -n HOST externaldns. You will end up with
      +a key printed to standard out like above (or in the case of dnssec-keygen in a
      +file called Kexternaldns......key).
    • +
    +

    BIND Configuration:

    +

    If you do not administer your own DNS, skip to RFC provider configuration

    +
      +
    • Edit your named.conf file (or appropriate included file) and add/change the
      +following.
    • +
    • Make sure You are listening on the right interfaces. At least whatever
      + interface external-dns will be communicating over and the interface that
      + faces the internet.
    • +
    • Add the key that you generated/was given to you above. Copy paste the four
      + lines that you got (not the same as the example key) into your file.
    • +
    • Create a zone for kubernetes. If you already have a zone, skip to the next
      + step. (I put the zone in it’s own subdirectory because named,
      + which shouldn’t be running as root, needs to create a journal file and the
      + default zone directory isn’t writeable by named).
      +
      zone "k8s.example.org" {
      +    type master;
      +    file "/etc/bind/pri/k8s/k8s.zone";
      +};
      +
    • +
    • Add your key to both transfer and update. For instance with our previous
      + zone.
      +
      zone "k8s.example.org" {
      +    type master;
      +    file "/etc/bind/pri/k8s/k8s.zone";
      +    allow-transfer {
      +        key "externaldns-key";
      +    };
      +    update-policy {
      +        grant externaldns-key zonesub ANY;
      +    };
      +};
      +
    • +
    • Create a zone file (k8s.zone):
      +
      $TTL 60 ; 1 minute
      +k8s.example.org         IN SOA  k8s.example.org. root.k8s.example.org. (
      +                                16         ; serial
      +                                60         ; refresh (1 minute)
      +                                60         ; retry (1 minute)
      +                                60         ; expire (1 minute)
      +                                60         ; minimum (1 minute)
      +                                )
      +                        NS      ns.k8s.example.org.
      +ns                      A       123.456.789.012
      +
    • +
    • Reload (or restart) named
    • +
    +

    Using external-dns

    +

    To use external-dns add an ingress or a LoadBalancer service with a host that
    +is part of the domain-filter. For example both of the following would produce
    +A records.
    +

    apiVersion: v1
    +kind: Service
    +metadata:
    +  name: nginx
    +  annotations:
    +    external-dns.alpha.kubernetes.io/hostname: svc.example.org
    +spec:
    +  type: LoadBalancer
    +  ports:
    +  - port: 80
    +    targetPort: 80
    +  selector:
    +    app: nginx
    +---
    +apiVersion: networking.k8s.io/v1
    +kind: Ingress
    +metadata:
    +    name: my-ingress
    +spec:
    +    rules:
    +    - host: ingress.example.org
    +      http:
    +          paths:
    +          - path: /
    +            backend:
    +                serviceName: my-service
    +                servicePort: 8000
    +

    +

    Custom TTL

    +

    The default DNS record TTL (Time-To-Live) is 0 seconds. You can customize this value by setting the annotation external-dns.alpha.kubernetes.io/ttl. e.g., modify the service manifest YAML file above:

    +
    apiVersion: v1
    +kind: Service
    +metadata:
    +  name: nginx
    +  annotations:
    +    external-dns.alpha.kubernetes.io/hostname: nginx.external-dns-test.my-org.com
    +    external-dns.alpha.kubernetes.io/ttl: 60
    +spec:
    +    ...
    +
    +

    This will set the DNS record’s TTL to 60 seconds.

    +

    A default TTL for all records can be set using the the flag with a time in seconds, minutes or hours, such as --rfc2136-min-ttl=60s

    +

    There are other annotation that can affect the generation of DNS records, but these are beyond the scope of this
    +tutorial and are covered in the main documentation.

    +

    Test with external-dns installed on local machine (optional)

    +

    You may install external-dns and test on a local machine by running:
    +external-dns --txt-owner-id k8s --provider rfc2136 --rfc2136-host=192.168.0.1 --rfc2136-port=53 --rfc2136-zone=k8s.example.org --rfc2136-tsig-secret=96Ah/a2g0/nLeFGK+d/0tzQcccf9hCEIy34PoXX2Qg8= --rfc2136-tsig-secret-alg=hmac-sha256 --rfc2136-tsig-keyname=externaldns-key --rfc2136-tsig-axfr --source ingress --once --domain-filter=k8s.example.org --dry-run
    +- host should be the IP of your master DNS server.
    +- tsig-secret should be changed to match your secret.
    +- tsig-keyname needs to match the keyname you used (if you changed it).
    +- domain-filter can be used as shown to filter the domains you wish to update.

    +

    RFC2136 provider configuration:

    +

    In order to use external-dns with your cluster you need to add a deployment
    +with access to your ingress and service resources. The following are two
    +example manifests with and without RBAC respectively.

    +
      +
    • +

      With RBAC:
      +

      apiVersion: v1
      +kind: Namespace
      +metadata:
      +  name: external-dns
      +  labels:
      +    name: external-dns
      +---
      +apiVersion: rbac.authorization.k8s.io/v1
      +kind: ClusterRole
      +metadata:
      +  name: external-dns
      +  namespace: external-dns
      +rules:
      +- apiGroups:
      +  - ""
      +  resources:
      +  - services
      +  - endpoints
      +  - pods
      +  - nodes
      +  verbs:
      +  - get
      +  - watch
      +  - list
      +- apiGroups:
      +  - extensions
      +  - networking.k8s.io
      +  resources:
      +  - ingresses
      +  verbs:
      +  - get
      +  - list
      +  - watch
      +---
      +apiVersion: v1
      +kind: ServiceAccount
      +metadata:
      +  name: external-dns
      +  namespace: external-dns
      +---
      +apiVersion: rbac.authorization.k8s.io/v1
      +kind: ClusterRoleBinding
      +metadata:
      +  name: external-dns-viewer
      +  namespace: external-dns
      +roleRef:
      +  apiGroup: rbac.authorization.k8s.io
      +  kind: ClusterRole
      +  name: external-dns
      +subjects:
      +- kind: ServiceAccount
      +  name: external-dns
      +  namespace: external-dns
      +---
      +apiVersion: apps/v1
      +kind: Deployment
      +metadata:
      +  name: external-dns
      +  namespace: external-dns
      +spec:
      +  selector:
      +    matchLabels:
      +      app: external-dns
      +  template:
      +    metadata:
      +      labels:
      +        app: external-dns
      +    spec:
      +      serviceAccountName: external-dns
      +      containers:
      +      - name: external-dns
      +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
      +        args:
      +        - --registry=txt
      +        - --txt-prefix=external-dns-
      +        - --txt-owner-id=k8s
      +        - --provider=rfc2136
      +        - --rfc2136-host=192.168.0.1
      +        - --rfc2136-port=53
      +        - --rfc2136-zone=k8s.example.org
      +        - --rfc2136-tsig-secret=96Ah/a2g0/nLeFGK+d/0tzQcccf9hCEIy34PoXX2Qg8=
      +        - --rfc2136-tsig-secret-alg=hmac-sha256
      +        - --rfc2136-tsig-keyname=externaldns-key
      +        - --rfc2136-tsig-axfr
      +        - --source=ingress
      +        - --domain-filter=k8s.example.org
      +

      +
    • +
    • +

      Without RBAC:
      +

      apiVersion: v1
      +kind: Namespace
      +metadata:
      +  name: external-dns
      +  labels:
      +    name: external-dns
      +---
      +apiVersion: apps/v1
      +kind: Deployment
      +metadata:
      +  name: external-dns
      +  namespace: external-dns
      +spec:
      +  selector:
      +    matchLabels:
      +      app: external-dns
      +  template:
      +    metadata:
      +      labels:
      +        app: external-dns
      +    spec:
      +      containers:
      +      - name: external-dns
      +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
      +        args:
      +        - --registry=txt
      +        - --txt-prefix=external-dns-
      +        - --txt-owner-id=k8s
      +        - --provider=rfc2136
      +        - --rfc2136-host=192.168.0.1
      +        - --rfc2136-port=53
      +        - --rfc2136-zone=k8s.example.org
      +        - --rfc2136-tsig-secret=96Ah/a2g0/nLeFGK+d/0tzQcccf9hCEIy34PoXX2Qg8=
      +        - --rfc2136-tsig-secret-alg=hmac-sha256
      +        - --rfc2136-tsig-keyname=externaldns-key
      +        - --rfc2136-tsig-axfr
      +        - --source=ingress
      +        - --domain-filter=k8s.example.org
      +

      +
    • +
    +

    Microsoft DNS (Insecure Updates)

    +

    While external-dns was not developed or tested against Microsoft DNS, it can be configured to work against it. YMMV.

    +

    Insecure Updates

    +

    DNS-side configuration

    +
      +
    1. Create a DNS zone
    2. +
    3. Enable insecure dynamic updates for the zone
    4. +
    5. Enable Zone Transfers to all servers
    6. +
    +

    external-dns configuration

    +

    You’ll want to configure external-dns similarly to the following:

    +
    ...
    +        - --provider=rfc2136
    +        - --rfc2136-host=192.168.0.1
    +        - --rfc2136-port=53
    +        - --rfc2136-zone=k8s.example.org
    +        - --rfc2136-insecure
    +        - --rfc2136-tsig-axfr # needed to enable zone transfers, which is required for deletion of records.
    +...
    +
    +

    Secure Updates Using RFC3645 (GSS-TSIG)

    +

    DNS-side configuration

    +
      +
    1. Create a DNS zone
    2. +
    3. Enable secure dynamic updates for the zone
    4. +
    5. Enable Zone Transfers to all servers
    6. +
    +

    If you see any error messages which indicate that external-dns was somehow not able to fetch
    +existing DNS records from your DNS server, this could mean that you forgot about step 3.

    +

    Kerberos Configuration

    +

    DNS with secure updates relies upon a valid Kerberos configuration running within the external-dns container. At this time, you will need to create a ConfigMap for the external-dns container to use and mount it in your deployment. Below is an example of a working Kerberos configuration inside a ConfigMap definition. This may be different depending on many factors in your environment:

    +

    apiVersion: v1
    +kind: ConfigMap
    +metadata:
    +  creationTimestamp: null
    +  name: krb5.conf
    +data:
    +  krb5.conf: |
    +    [logging]
    +    default = FILE:/var/log/krb5libs.log
    +    kdc = FILE:/var/log/krb5kdc.log
    +    admin_server = FILE:/var/log/kadmind.log
    +
    +    [libdefaults]
    +    dns_lookup_realm = false
    +    ticket_lifetime = 24h
    +    renew_lifetime = 7d
    +    forwardable = true
    +    rdns = false
    +    pkinit_anchors = /etc/pki/tls/certs/ca-bundle.crt
    +    default_ccache_name = KEYRING:persistent:%{uid}
    +
    +    default_realm = YOUR-REALM.COM
    +
    +    [realms]
    +    YOUR-REALM.COM = {
    +      kdc = dc1.yourdomain.com
    +      admin_server = dc1.yourdomain.com
    +    }
    +
    +    [domain_realm]
    +    yourdomain.com = YOUR-REALM.COM
    +    .yourdomain.com = YOUR-REALM.COM
    +

    +In most cases, the realm name will probably be the same as the domain name, so you can simply replace
    +YOUR-REALM.COM with something like YOURDOMAIN.COM.

    +

    Once the ConfigMap is created, the container external-dns container needs to be told to mount that ConfigMap as a volume at the default Kerberos configuration location. The pod spec should include a similar configuration to the following:

    +
    ...
    +    volumeMounts:
    +    - mountPath: /etc/krb5.conf
    +      name: kerberos-config-volume
    +      subPath: krb5.conf
    +...
    +  volumes:
    +  - configMap:
    +      defaultMode: 420
    +      name: krb5.conf
    +    name: kerberos-config-volume
    +...
    +
    +

    external-dns configuration

    +

    You’ll want to configure external-dns similarly to the following:

    +
    ...
    +        - --provider=rfc2136
    +     - --rfc2136-gss-tsig
    +        - --rfc2136-host=dns-host.yourdomain.com
    +        - --rfc2136-port=53
    +        - --rfc2136-zone=your-zone.com
    +        - --rfc2136-kerberos-username=your-domain-account
    +        - --rfc2136-kerberos-password=your-domain-password
    +        - --rfc2136-kerberos-realm=your-domain.com
    +        - --rfc2136-tsig-axfr # needed to enable zone transfers, which is required for deletion of records.
    +...
    +
    +

    As noted above, the --rfc2136-kerberos-realm flag is completely optional and won’t be necessary in many cases.
    +Most likely, you will only need it if you see errors similar to this: KRB Error: (68) KDC_ERR_WRONG_REALM Reserved for future use.

    +

    The flag --rfc2136-host can be set to the host’s domain name or IP address.
    +However, it also determines the name of the Kerberos principal which is used during authentication.
    +This means that Active Directory might only work if this is set to a specific domain name, possibly leading to errors like this:
    +KDC_ERR_S_PRINCIPAL_UNKNOWN Server not found in Kerberos database.
    +To fix this, try setting --rfc2136-host to the “actual” hostname of your DNS server.

    + +
    +
    + + + Last update: + May 29, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/scaleway/index.html b/v0.13.6/tutorials/scaleway/index.html new file mode 100644 index 0000000000..30460c0dd2 --- /dev/null +++ b/v0.13.6/tutorials/scaleway/index.html @@ -0,0 +1,2252 @@ + + + + + + + + + + + + + + + + + + Setting up ExternalDNS for Services on Scaleway - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + + + + +
    +
    + + + + + + + + +

    Setting up ExternalDNS for Services on Scaleway

    +

    This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using Scaleway DNS.

    +

    Make sure to use >=0.7.4 version of ExternalDNS for this tutorial.

    +

    Warning: Scaleway DNS is currently in Public Beta and may not be suited for production usage.

    +

    Importing a Domain into Scaleway DNS

    +

    In order to use your domain, you need to import it into Scaleway DNS. If it’s not already done, you can follow this documentation

    +

    Once the domain is imported you can either use the root zone, or create a subzone to use.

    +

    In this example we will use example.com as an example.

    +

    Creating Scaleway Credentials

    +

    To use ExternalDNS with Scaleway DNS, you need to create an API token (composed of the Access Key and the Secret Key).
    +You can either use existing ones or you can create a new token, as explained in How to generate an API token or directly by going to the credentials page.

    +

    Two environment variables are needed to run ExternalDNS with Scaleway DNS:
    +- SCW_ACCESS_KEY which is the Access Key.
    +- SCW_SECRET_KEY which is the Secret Key.

    +

    Deploy ExternalDNS

    +

    Connect your kubectl client to the cluster you want to test ExternalDNS with.
    +Then apply one of the following manifests file to deploy ExternalDNS.

    +

    The following example are suited for development. For a production usage, prefer secrets over environment, and use a tagged release.

    +

    Manifest (for clusters without RBAC enabled)

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  replicas: 1
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  strategy:
    +    type: Recreate
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service # ingress is also possible
    +        - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
    +        - --provider=scaleway
    +        env:
    +        - name: SCW_ACCESS_KEY
    +          value: "<your access key>"
    +        - name: SCW_SECRET_KEY
    +          value: "<your secret key>"
    +
    +

    Manifest (for clusters with RBAC enabled)

    +
    apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRole
    +metadata:
    +  name: external-dns
    +rules:
    +- apiGroups: [""]
    +  resources: ["services","endpoints","pods"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: ["extensions"]
    +  resources: ["ingresses"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: [""]
    +  resources: ["nodes"]
    +  verbs: ["list","watch"]
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRoleBinding
    +metadata:
    +  name: external-dns-viewer
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: ClusterRole
    +  name: external-dns
    +subjects:
    +- kind: ServiceAccount
    +  name: external-dns
    +  namespace: default
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  replicas: 1
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  strategy:
    +    type: Recreate
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      serviceAccountName: external-dns
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service # ingress is also possible
    +        - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
    +        - --provider=scaleway
    +        env:
    +        - name: SCW_ACCESS_KEY
    +          value: "<your access key>"
    +        - name: SCW_SECRET_KEY
    +          value: "<your secret key>"
    +
    +

    Deploying an Nginx Service

    +

    Create a service file called ‘nginx.yaml’ with the following contents:

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: nginx
    +spec:
    +  replicas: 1
    +  selector:
    +    matchLabels:
    +      app: nginx
    +  template:
    +    metadata:
    +      labels:
    +        app: nginx
    +    spec:
    +      containers:
    +      - image: nginx
    +        name: nginx
    +        ports:
    +        - containerPort: 80
    +---
    +apiVersion: v1
    +kind: Service
    +metadata:
    +  name: nginx
    +  annotations:
    +    external-dns.alpha.kubernetes.io/hostname: my-app.example.com
    +spec:
    +  selector:
    +    app: nginx
    +  type: LoadBalancer
    +  ports:
    +    - protocol: TCP
    +      port: 80
    +      targetPort: 80
    +
    +

    Note the annotation on the service; use the same hostname as the Scaleway DNS zone created above.

    +

    ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records.

    +

    Create the deployment and service:

    +
    $ kubectl create -f nginx.yaml
    +
    +

    Depending where you run your service it can take a little while for your cloud provider to create an external IP for the service.

    +

    Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize the Scaleway DNS records.

    +

    Verifying Scaleway DNS records

    +

    Check your Scaleway DNS UI to view the records for your Scaleway DNS zone.

    +

    Click on the zone for the one created above if a different domain was used.

    +

    This should show the external IP address of the service as the A record for your domain.

    +

    Cleanup

    +

    Now that we have verified that ExternalDNS will automatically manage Scaleway DNS records, we can delete the tutorial’s example:

    +
    $ kubectl delete service -f nginx.yaml
    +$ kubectl delete service -f externaldns.yaml
    +
    + +
    +
    + + + Last update: + June 28, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/security-context/index.html b/v0.13.6/tutorials/security-context/index.html new file mode 100644 index 0000000000..914e3a969d --- /dev/null +++ b/v0.13.6/tutorials/security-context/index.html @@ -0,0 +1,1960 @@ + + + + + + + + + + + + + + + + + + Running ExternalDNS with limited privileges - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + +
    +
    +
    + + + +
    +
    +
    + + +
    +
    + + + + + + + + +

    Running ExternalDNS with limited privileges

    +

    You can run ExternalDNS with reduced privileges since v0.5.6 using the following SecurityContext.

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - ... # your arguments here
    +        securityContext:
    +          runAsNonRoot: true
    +          runAsUser: 65534
    +          readOnlyRootFilesystem: true
    +          capabilities:
    +            drop: ["ALL"]
    +
    + +
    +
    + + + Last update: + May 29, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/tencentcloud/index.html b/v0.13.6/tutorials/tencentcloud/index.html new file mode 100644 index 0000000000..86e21ab2d4 --- /dev/null +++ b/v0.13.6/tutorials/tencentcloud/index.html @@ -0,0 +1,2207 @@ + + + + + + + + + + + + + + + + + + Setting up ExternalDNS for Tencent Cloud - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + +
    +
    +
    + + + +
    +
    +
    + + +
    +
    + + + + + + + + +

    Setting up ExternalDNS for Tencent Cloud

    +

    External Dns Version

    +
      +
    • Make sure to use >=0.13.1 version of ExternalDNS for this tutorial
    • +
    +

    Set up PrivateDns or DNSPod

    +

    Tencent Cloud DNSPod Service is the domain name resolution and management service for public access.
    +Tencent Cloud PrivateDNS Service is the domain name resolution and management service for VPC internal access.

    +
      +
    • +

      If you want to use internal dns service in Tencent Cloud.
      +1. Set up the args --tencent-cloud-zone-type=private
      +2. Create a DNS domain in PrivateDNS console. DNS domain which will contain the managed DNS records.

      +
    • +
    • +

      If you want to use public dns service in Tencent Cloud.
      +1. Set up the args --tencent-cloud-zone-type=public
      +2. Create a Domain in DnsPod console. DNS domain which will contain the managed DNS records.

      +
    • +
    +

    Set up CAM for API Key

    +

    In Tencent CAM Console. you may get the secretId and secretKey pair. make sure the key pair has those Policy.
    +

    {
    +    "version": "2.0",
    +    "statement": [
    +        {
    +            "effect": "allow",
    +            "action": [
    +                "dnspod:ModifyRecord",
    +                "dnspod:DeleteRecord",
    +                "dnspod:CreateRecord",
    +                "dnspod:DescribeRecordList",
    +                "dnspod:DescribeDomainList"
    +            ],
    +            "resource": [
    +                "*"
    +            ]
    +        },
    +        {
    +            "effect": "allow",
    +            "action": [
    +                "privatedns:DescribePrivateZoneList",
    +                "privatedns:DescribePrivateZoneRecordList",
    +                "privatedns:CreatePrivateZoneRecord",
    +                "privatedns:DeletePrivateZoneRecord",
    +                "privatedns:ModifyPrivateZoneRecord"
    +            ],
    +            "resource": [
    +                "*"
    +            ]
    +        }
    +    ]
    +}
    +

    +

    Deploy ExternalDNS

    +

    Manifest (for clusters with RBAC enabled)

    +
    apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRole
    +metadata:
    +  name: external-dns
    +rules:
    +- apiGroups: [""]
    +  resources: ["services","endpoints","pods"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: ["extensions","networking.k8s.io"]
    +  resources: ["ingresses"] 
    +  verbs: ["get","watch","list"]
    +- apiGroups: [""]
    +  resources: ["nodes"]
    +  verbs: ["list"]
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRoleBinding
    +metadata:
    +  name: external-dns-viewer
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: ClusterRole
    +  name: external-dns
    +subjects:
    +- kind: ServiceAccount
    +  name: external-dns
    +  namespace: default
    +---
    +apiVersion: v1
    +kind: ConfigMap
    +metadata:
    +  name: external-dns
    +data:
    +  tencent-cloud.json: |
    +    {
    +      "regionId": "ap-shanghai",
    +      "secretId": "******",  
    +      "secretKey": "******",
    +      "vpcId": "vpc-******",
    +      "internetEndpoint": false  # Default: false. Access the Tencent API through the intranet. If you need to deploy on the public network, you need to change to true
    +    }
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      containers:
    +      - args:
    +        - --source=service
    +        - --source=ingress
    +        - --domain-filter=external-dns-test.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones
    +        - --provider=tencentcloud
    +        - --policy=sync # set `upsert-only` would prevent ExternalDNS from deleting any records
    +        - --tencent-cloud-zone-type=private # only look at private hosted zones. set `public` to use the public dns service.
    +        - --tencent-cloud-config-file=/etc/kubernetes/tencent-cloud.json
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        imagePullPolicy: Always
    +        name: external-dns
    +        resources: {}
    +        terminationMessagePath: /dev/termination-log
    +        terminationMessagePolicy: File
    +        volumeMounts:
    +        - mountPath: /etc/kubernetes
    +          name: config-volume
    +          readOnly: true
    +      dnsPolicy: ClusterFirst
    +      hostAliases:
    +      - hostnames:
    +        - privatedns.internal.tencentcloudapi.com
    +        - dnspod.internal.tencentcloudapi.com
    +        ip: 169.254.0.95
    +      restartPolicy: Always
    +      schedulerName: default-scheduler
    +      securityContext: {}
    +      serviceAccount: external-dns
    +      serviceAccountName: external-dns
    +      terminationGracePeriodSeconds: 30
    +      volumes:
    +      - configMap:
    +          defaultMode: 420
    +          items:
    +          - key: tencent-cloud.json
    +            path: tencent-cloud.json
    +          name: external-dns
    +        name: config-volume
    +
    +

    Example

    +

    Service

    +
    apiVersion: v1
    +kind: Service
    +metadata:
    +  name: nginx
    +  annotations:
    +    external-dns.alpha.kubernetes.io/hostname: nginx.external-dns-test.com
    +    external-dns.alpha.kubernetes.io/internal-hostname: nginx-internal.external-dns-test.com
    +    external-dns.alpha.kubernetes.io/ttl: "600"
    +spec:
    +  type: LoadBalancer
    +  ports:
    +  - port: 80
    +    name: http
    +    targetPort: 80
    +  selector:
    +    app: nginx
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: nginx
    +spec:
    +  selector:
    +    matchLabels:
    +      app: nginx
    +  template:
    +    metadata:
    +      labels:
    +        app: nginx
    +    spec:
    +      containers:
    +      - image: nginx
    +        name: nginx
    +        ports:
    +        - containerPort: 80
    +          name: http
    +
    +

    nginx.external-dns-test.com will record to the Loadbalancer VIP.
    +nginx-internal.external-dns-test.com will record to the ClusterIP.
    +all of the DNS Record ttl will be 600.

    +

    Attention

    +

    This makes ExternalDNS safe for running in environments where there are other records managed via other means.

    + +
    +
    + + + Last update: + May 29, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/traefik-proxy/index.html b/v0.13.6/tutorials/traefik-proxy/index.html new file mode 100644 index 0000000000..8a3d03f936 --- /dev/null +++ b/v0.13.6/tutorials/traefik-proxy/index.html @@ -0,0 +1,2140 @@ + + + + + + + + + + + + + + + + + + Configuring ExternalDNS to use the Traefik Proxy Source - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + +
    + +
    + + +
    +
    + + + + + + + + +

    Configuring ExternalDNS to use the Traefik Proxy Source

    +

    This tutorial describes how to configure ExternalDNS to use the Traefik Proxy source.
    +It is meant to supplement the other provider-specific setup tutorials.

    +

    Manifest (for clusters without RBAC enabled)

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      containers:
    +      - name: external-dns
    +        # update this to the desired external-dns version
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.3
    +        args:
    +        - --source=traefik-proxy
    +        - --provider=aws
    +        - --registry=txt
    +        - --txt-owner-id=my-identifier
    +
    +

    Manifest (for clusters with RBAC enabled)

    +
    apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRole
    +metadata:
    +  name: external-dns
    +rules:
    +- apiGroups: [""]
    +  resources: ["services","endpoints","pods"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: [""]
    +  resources: ["nodes"]
    +  verbs: ["list","watch"]
    +- apiGroups: ["traefik.containo.us","traefik.io"]
    +  resources: ["ingressroutes", "ingressroutetcps", "ingressrouteudps"]
    +  verbs: ["get","watch","list"]
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRoleBinding
    +metadata:
    +  name: external-dns-viewer
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: ClusterRole
    +  name: external-dns
    +subjects:
    +- kind: ServiceAccount
    +  name: external-dns
    +  namespace: default
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      serviceAccountName: external-dns
    +      containers:
    +      - name: external-dns
    +        # update this to the desired external-dns version
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.3
    +        args:
    +        - --source=traefik-proxy
    +        - --provider=aws
    +        - --registry=txt
    +        - --txt-owner-id=my-identifier
    +
    +

    Deploying a Traefik IngressRoute

    +

    Create a IngressRoute file called ‘traefik-ingress.yaml’ with the following contents:
    +

    apiVersion: traefik.io/v1alpha1
    +kind: IngressRoute
    +metadata:
    +  name: traefik-ingress
    +  annotations:
    +    external-dns.alpha.kubernetes.io/target: traefik.example.com
    +    kubernetes.io/ingress.class: traefik
    +spec:
    +  entryPoints:
    +    - web
    +    - websecure
    +  routes:
    +    - match: Host(`application.example.com`)
    +      kind: Rule
    +      services:
    +        - name: service
    +          namespace: namespace
    +          port: port
    +

    +

    Note the annotation on the IngressRoute (external-dns.alpha.kubernetes.io/target); use the same hostname as the traefik DNS.

    +

    ExternalDNS uses this annotation to determine what services should be registered with DNS.

    +

    Create the IngressRoute:

    +
    $ kubectl create -f traefik-ingress.yaml
    +
    +

    Depending where you run your IngressRoute it can take a little while for ExternalDNS synchronize the DNS record.

    +

    Cleanup

    +

    Now that we have verified that ExternalDNS will automatically manage Traefik DNS records, we can delete the tutorial’s example:

    +
    $ kubectl delete -f traefik-ingress.yaml
    +$ kubectl delete -f externaldns.yaml
    +
    + +
    +
    + + + Last update: + August 10, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/transip/index.html b/v0.13.6/tutorials/transip/index.html new file mode 100644 index 0000000000..7631063c0e --- /dev/null +++ b/v0.13.6/tutorials/transip/index.html @@ -0,0 +1,2217 @@ + + + + + + + + + + + + + + + + + + Setting up ExternalDNS for Services on TransIP - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + + + + +
    +
    + + + + + + + + +

    Setting up ExternalDNS for Services on TransIP

    +

    This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using TransIP.

    +

    Make sure to use >=0.5.14 version of ExternalDNS for this tutorial, have at least 1 domain registered at TransIP and enabled the API.

    +

    Enable TransIP API and prepare your API key

    +

    To use the TransIP API you need an account at TransIP and enable API usage as described in the knowledge base. With the private key generated by the API, we create a kubernetes secret:

    +
    $ kubectl create secret generic transip-api-key --from-file=transip-api-key=/path/to/private.key
    +
    +

    Deploy ExternalDNS

    +

    Below are example manifests, for both cluster without or with RBAC enabled. Don’t forget to replace YOUR_TRANSIP_ACCOUNT_NAME with your TransIP account name. In these examples, an example domain-filter is defined. Such a filter can be used to prevent ExternalDNS from touching any domain not listed in the filter. Refer to the docs for any other command-line parameters you might want to use.

    +

    Manifest (for clusters without RBAC enabled)

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service # ingress is also possible
    +        - --domain-filter=example.com # (optional) limit to only example.com domains
    +        - --provider=transip
    +        - --transip-account=YOUR_TRANSIP_ACCOUNT_NAME
    +        - --transip-keyfile=/transip/transip-api-key
    +        volumeMounts:
    +        - mountPath: /transip
    +          name: transip-api-key
    +          readOnly: true
    +      volumes:
    +      - name: transip-api-key
    +        secret:
    +          secretName: transip-api-key
    +
    +

    Manifest (for clusters with RBAC enabled)

    +
    apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRole
    +metadata:
    +  name: external-dns
    +rules:
    +- apiGroups: [""]
    +  resources: ["services","endpoints","pods"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: ["extensions","networking.k8s.io"]
    +  resources: ["ingresses"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: [""]
    +  resources: ["nodes"]
    +  verbs: ["watch", "list"]
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRoleBinding
    +metadata:
    +  name: external-dns-viewer
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: ClusterRole
    +  name: external-dns
    +subjects:
    +- kind: ServiceAccount
    +  name: external-dns
    +  namespace: default
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      serviceAccountName: external-dns
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service # ingress is also possible
    +        - --domain-filter=example.com # (optional) limit to only example.com domains
    +        - --provider=transip
    +        - --transip-account=YOUR_TRANSIP_ACCOUNT_NAME
    +        - --transip-keyfile=/transip/transip-api-key
    +        volumeMounts:
    +        - mountPath: /transip
    +          name: transip-api-key
    +          readOnly: true
    +      volumes:
    +      - name: transip-api-key
    +        secret:
    +          secretName: transip-api-key
    +
    +

    Deploying an Nginx Service

    +

    Create a service file called ‘nginx.yaml’ with the following contents:

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: nginx
    +spec:
    +  selector:
    +    matchLabels:
    +      app: nginx
    +  template:
    +    metadata:
    +      labels:
    +        app: nginx
    +    spec:
    +      containers:
    +      - image: nginx
    +        name: nginx
    +        ports:
    +        - containerPort: 80
    +---
    +apiVersion: v1
    +kind: Service
    +metadata:
    +  name: nginx
    +  annotations:
    +    external-dns.alpha.kubernetes.io/hostname: my-app.example.com
    +spec:
    +  selector:
    +    app: nginx
    +  type: LoadBalancer
    +  ports:
    +    - protocol: TCP
    +      port: 80
    +      targetPort: 80
    +
    +

    Note the annotation on the service; this is the name ExternalDNS will create and manage DNS records for.

    +

    ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records.

    +

    Create the deployment and service:

    +
    $ kubectl create -f nginx.yaml
    +
    +

    Depending where you run your service it can take a little while for your cloud provider to create an external IP for the service.

    +

    Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize the TransIP DNS records.

    +

    Verifying TransIP DNS records

    +

    Check your TransIP Control Panel to view the records for your TransIP DNS zone.

    +

    Click on the zone for the one created above if a different domain was used.

    +

    This should show the external IP address of the service as the A record for your domain.

    + +
    +
    + + + Last update: + May 29, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/ultradns/index.html b/v0.13.6/tutorials/ultradns/index.html new file mode 100644 index 0000000000..13a995640e --- /dev/null +++ b/v0.13.6/tutorials/ultradns/index.html @@ -0,0 +1,2770 @@ + + + + + + + + + + + + + + + + + + Setting up ExternalDNS for Services on UltraDNS - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + + + + +
    +
    + + + + + + + + +

    Setting up ExternalDNS for Services on UltraDNS

    +

    This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using UltraDNS.

    +

    For this tutorial, please make sure that you are using a version > 0.7.2 of ExternalDNS.

    +

    Managing DNS with UltraDNS

    +

    If you would like to read-up on the UltraDNS service, you can find additional details here: Introduction to UltraDNS

    +

    Before proceeding, please create a new DNS Zone that you will create your records in for this tutorial process. For the examples in this tutorial, we will be using example.com as our Zone.

    +

    Setting Up UltraDNS Credentials

    +

    The following environment variables will be needed to run ExternalDNS with UltraDNS.

    +

    ULTRADNS_USERNAME,ULTRADNS_PASSWORD, &ULTRADNS_BASEURL
    +ULTRADNS_ACCOUNTNAME(optional variable).

    +

    Deploying ExternalDNS

    +

    Connect your kubectl client to the cluster you want to test ExternalDNS with.
    +Then, apply one of the following manifests file to deploy ExternalDNS.

    +
      +
    • Note: We are assuming the zone is already present within UltraDNS.
    • +
    • Note: While creating CNAMES as target endpoints, the --txt-prefix option is mandatory.
    • +
    +

    Manifest (for clusters without RBAC enabled)

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service 
    +        - --source=ingress # ingress is also possible
    +        - --domain-filter=example.com # (Recommended) We recommend to use this filter as it minimize the time to propagate changes, as there are less number of zones to look into..
    +        - --provider=ultradns
    +        - --txt-prefix=txt-
    +        env:
    +        - name: ULTRADNS_USERNAME
    +          value: ""
    +        - name: ULTRADNS_PASSWORD  # The password is required to be BASE64 encrypted.
    +          value: ""
    +        - name: ULTRADNS_BASEURL
    +          value: "https://api.ultradns.com/"
    +        - name: ULTRADNS_ACCOUNTNAME
    +          value: ""
    +
    +

    Manifest (for clusters with RBAC enabled)

    +
    apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRole
    +metadata:
    +  name: external-dns
    +rules:
    +- apiGroups: [""]
    +  resources: ["services","endpoints","pods"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: ["extensions"]
    +  resources: ["ingresses"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: [""]
    +  resources: ["nodes"]
    +  verbs: ["list","watch"]
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRoleBinding
    +metadata:
    +  name: external-dns-viewer
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: ClusterRole
    +  name: external-dns
    +subjects:
    +- kind: ServiceAccount
    +  name: external-dns
    +  namespace: default
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      serviceAccountName: external-dns
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service 
    +        - --source=ingress
    +        - --domain-filter=example.com #(Recommended) We recommend to use this filter as it minimize the time to propagate changes, as there are less number of zones to look into..
    +        - --provider=ultradns
    +        - --txt-prefix=txt-
    +        env:
    +        - name: ULTRADNS_USERNAME
    +          value: ""
    +        - name: ULTRADNS_PASSWORD # The password is required to be BASE64 encrypted.
    +          value: ""
    +        - name: ULTRADNS_BASEURL
    +          value: "https://api.ultradns.com/"
    +        - name: ULTRADNS_ACCOUNTNAME
    +          value: ""
    +
    +

    Deploying an Nginx Service

    +

    Create a service file called ‘nginx.yaml’ with the following contents:

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: nginx
    +spec:
    +  selector:
    +    matchLabels:
    +      app: nginx
    +  template:
    +    metadata:
    +      labels:
    +        app: nginx
    +    spec:
    +      containers:
    +      - image: nginx
    +        name: nginx
    +        ports:
    +        - containerPort: 80
    +---
    +apiVersion: v1
    +kind: Service
    +metadata:
    +  name: nginx
    +  annotations:
    +    external-dns.alpha.kubernetes.io/hostname: my-app.example.com.
    +spec:
    +  selector:
    +    app: nginx
    +  type: LoadBalancer
    +  ports:
    +    - protocol: TCP
    +      port: 80
    +      targetPort: 80
    +
    +

    Please note the annotation on the service. Use the same hostname as the UltraDNS zone created above.

    +

    ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records.

    +

    Creating the Deployment and Service:

    +
    $ kubectl create -f nginx.yaml
    +$ kubectl create -f external-dns.yaml
    +
    +

    Depending on where you run your service from, it can take a few minutes for your cloud provider to create an external IP for the service.

    +

    Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and will synchronize the UltraDNS records.

    +

    Verifying UltraDNS Records

    +

    Please verify on the UltraDNS UI that the records are created under the zone “example.com”.

    +

    For more information on UltraDNS UI, refer to (https://docs.ultradns.neustar/mspuserguide.html).

    +

    Select the zone that was created above (or select the appropriate zone if a different zone was used.)

    +

    The external IP address will be displayed as a CNAME record for your zone.

    +

    Cleaning Up the Deployment and Service

    +

    Now that we have verified that ExternalDNS will automatically manage your UltraDNS records, you can delete example zones that you created in this tutorial:

    +
    $ kubectl delete service -f nginx.yaml
    +$ kubectl delete service -f externaldns.yaml
    +
    +

    Examples to Manage your Records

    +

    Creating Multiple A Records Target

    +
      +
    • First, you want to create a service file called ‘apple-banana-echo.yaml’
      +
      ---
      +kind: Pod
      +apiVersion: v1
      +metadata:
      +  name: example-app
      +  labels:
      +    app: apple
      +spec:
      +  containers:
      +    - name: example-app
      +      image: hashicorp/http-echo
      +      args:
      +        - "-text=apple"
      +---
      +kind: Service
      +apiVersion: v1
      +metadata:
      +  name: example-service
      +spec:
      +  selector:
      +    app: apple
      +  ports:
      +    - port: 5678 # Default port for image
      +
    • +
    • Then, create service file called ‘expose-apple-banana-app.yaml’ to expose the services. For more information to deploy ingress controller, refer to (https://kubernetes.github.io/ingress-nginx/deploy/)
      +
      apiVersion: networking.k8s.io/v1
      +kind: Ingress
      +metadata:
      +  name: example-ingress
      +  annotations:
      +    ingress.kubernetes.io/rewrite-target: /
      +    ingress.kubernetes.io/scheme: internet-facing
      +    external-dns.alpha.kubernetes.io/hostname: apple.example.com.
      +    external-dns.alpha.kubernetes.io/target: 10.10.10.1,10.10.10.23
      +spec:
      +  rules:
      +  - http:
      +      paths:
      +        - path: /apple
      +          pathType: Prefix
      +          backend:
      +            service:
      +              name: example-service
      +              port:
      +                number: 5678
      +
    • +
    • Then, create the deployment and service:
      +
      $ kubectl create -f apple-banana-echo.yaml
      +$ kubectl create -f expose-apple-banana-app.yaml
      +$ kubectl create -f external-dns.yaml
      +
    • +
    • Depending on where you run your service from, it can take a few minutes for your cloud provider to create an external IP for the service.
    • +
    • Please verify on the UltraDNS UI that the records have been created under the zone “example.com”.
    • +
    • Finally, you will need to clean up the deployment and service. Please verify on the UI afterwards that the records have been deleted from the zone “example.com”:
      +
      $ kubectl delete -f apple-banana-echo.yaml
      +$ kubectl delete -f expose-apple-banana-app.yaml
      +$ kubectl delete -f external-dns.yaml
      +
    • +
    +

    Creating CNAME Record

    +
      +
    • Please note, that prior to deploying the external-dns service, you will need to add the option –txt-prefix=txt- into external-dns.yaml. If this not provided, your records will not be created.
    • +
    • First, create a service file called ‘apple-banana-echo.yaml’
        +
      • Config File Example – kubernetes cluster is on-premise not on cloud
        +
        ---
        +kind: Pod
        +apiVersion: v1
        +metadata:
        +  name: example-app
        +  labels:
        +    app: apple
        +spec:
        +  containers:
        +    - name: example-app
        +      image: hashicorp/http-echo
        +      args:
        +        - "-text=apple"
        +---
        +kind: Service
        +apiVersion: v1
        +metadata:
        +  name: example-service
        +spec:
        +  selector:
        +    app: apple
        +  ports:
        +    - port: 5678 # Default port for image
        +---
        +apiVersion: networking.k8s.io/v1
        +kind: Ingress
        +metadata:
        +  name: example-ingress
        +  annotations:
        +    ingress.kubernetes.io/rewrite-target: /
        +    ingress.kubernetes.io/scheme: internet-facing
        +    external-dns.alpha.kubernetes.io/hostname: apple.example.com.
        +    external-dns.alpha.kubernetes.io/target: apple.cname.com.
        +spec:
        +  rules:
        +  - http:
        +      paths:
        +        - path: /apple
        +          backend:
        +            service:
        +              name: example-service
        +              port:
        +                number: 5678
        +
      • +
      • Config File Example – Kubernetes cluster service from different cloud vendors
        +
        ---
        +kind: Pod
        +apiVersion: v1
        +metadata:
        +  name: example-app
        +  labels:
        +    app: apple
        +spec:
        +  containers:
        +    - name: example-app
        +      image: hashicorp/http-echo
        +      args:
        +        - "-text=apple"
        +---
        +kind: Service
        +apiVersion: v1
        +metadata:
        +  name: example-service
        +  annotations:
        +    external-dns.alpha.kubernetes.io/hostname: my-app.example.com.
        +spec:
        +  selector:
        +    app: apple
        +  type: LoadBalancer
        +  ports:
        +    - protocol: TCP
        +      port: 5678
        +      targetPort: 5678
        +
      • +
      +
    • +
    • Then, create the deployment and service:
      +
      $ kubectl create -f apple-banana-echo.yaml
      +$ kubectl create -f external-dns.yaml
      +
    • +
    • Depending on where you run your service from, it can take a few minutes for your cloud provider to create an external IP for the service.
    • +
    • Please verify on the UltraDNS UI, that the records have been created under the zone “example.com”.
    • +
    • Finally, you will need to clean up the deployment and service. Please verify on the UI afterwards that the records have been deleted from the zone “example.com”:
      +
      $ kubectl delete -f apple-banana-echo.yaml
      +$ kubectl delete -f external-dns.yaml
      +
    • +
    +

    Creating Multiple Types Of Records

    +
      +
    • Please note, that prior to deploying the external-dns service, you will need to add the option –txt-prefix=txt- into external-dns.yaml. Since you will also be created a CNAME record, If this not provided, your records will not be created.
    • +
    • First, create a service file called ‘apple-banana-echo.yaml’
        +
      • Config File Example – kubernetes cluster is on-premise not on cloud
        +
        ---
        +kind: Pod
        +apiVersion: v1
        +metadata:
        +  name: example-app
        +  labels:
        +    app: apple
        +spec:
        +  containers:
        +    - name: example-app
        +      image: hashicorp/http-echo
        +      args:
        +        - "-text=apple"
        +---
        +kind: Service
        +apiVersion: v1
        +metadata:
        +  name: example-service
        +spec:
        +  selector:
        +    app: apple
        +  ports:
        +    - port: 5678 # Default port for image
        +---
        +kind: Pod
        +apiVersion: v1
        +metadata:
        +  name: example-app1
        +  labels:
        +    app: apple1
        +spec:
        +  containers:
        +    - name: example-app1
        +      image: hashicorp/http-echo
        +      args:
        +        - "-text=apple"
        +---
        +kind: Service
        +apiVersion: v1
        +metadata:
        +  name: example-service1
        +spec:
        +  selector:
        +    app: apple1
        +  ports:
        +    - port: 5679 # Default port for image
        +---
        +kind: Pod
        +apiVersion: v1
        +metadata:
        +  name: example-app2
        +  labels:
        +    app: apple2
        +spec:
        +  containers:
        +    - name: example-app2
        +      image: hashicorp/http-echo
        +      args:
        +        - "-text=apple"
        +---
        +kind: Service
        +apiVersion: v1
        +metadata:
        +  name: example-service2
        +spec:
        +  selector:
        +    app: apple2
        +  ports:
        +    - port: 5680 # Default port for image
        +---
        +apiVersion: networking.k8s.io/v1
        +kind: Ingress
        +metadata:
        +  name: example-ingress
        +  annotations:
        +    ingress.kubernetes.io/rewrite-target: /
        +    ingress.kubernetes.io/scheme: internet-facing
        +    external-dns.alpha.kubernetes.io/hostname: apple.example.com.
        +    external-dns.alpha.kubernetes.io/target: apple.cname.com.
        +spec:
        +  rules:
        +  - http:
        +      paths:
        +        - path: /apple
        +          backend:
        +            service:
        +              name: example-service
        +              port:
        +                number: 5678
        +---
        +apiVersion: networking.k8s.io/v1
        +kind: Ingress
        +metadata:
        +  name: example-ingress1
        +  annotations:
        +    ingress.kubernetes.io/rewrite-target: /
        +    ingress.kubernetes.io/scheme: internet-facing
        +    external-dns.alpha.kubernetes.io/hostname: apple-banana.example.com.
        +    external-dns.alpha.kubernetes.io/target: 10.10.10.3
        +spec:
        +  rules:
        +  - http:
        +      paths:
        +        - path: /apple
        +          backend:
        +            service:
        +              name: example-service1
        +              port:
        +                number: 5679
        +---
        +apiVersion: networking.k8s.io/v1
        +kind: Ingress
        +metadata:
        +  name: example-ingress2
        +  annotations:
        +    ingress.kubernetes.io/rewrite-target: /
        +    ingress.kubernetes.io/scheme: internet-facing
        +    external-dns.alpha.kubernetes.io/hostname: banana.example.com.
        +    external-dns.alpha.kubernetes.io/target: 10.10.10.3,10.10.10.20
        +spec:
        +  rules:
        +  - http:
        +      paths:
        +        - path: /apple
        +          backend:
        +            service:
        +              name: example-service2
        +              port:
        +                number: 5680
        +
      • +
      • Config File Example – Kubernetes cluster service from different cloud vendors
        +
        ---
        +apiVersion: apps/v1
        +kind: Deployment
        +metadata:
        +  name: nginx
        +spec:
        +  selector:
        +    matchLabels:
        +      app: nginx
        +  template:
        +    metadata:
        +      labels:
        +        app: nginx
        +    spec:
        +      containers:
        +      - image: nginx
        +        name: nginx
        +        ports:
        +        - containerPort: 80
        +---
        +apiVersion: v1
        +kind: Service
        +metadata:
        +  name: nginx
        +  annotations:
        +    external-dns.alpha.kubernetes.io/hostname: my-app.example.com.
        +spec:
        +  selector:
        +    app: nginx
        +  type: LoadBalancer
        +  ports:
        +    - protocol: TCP
        +      port: 80
        +      targetPort: 80
        +---
        +kind: Pod
        +apiVersion: v1
        +metadata:
        +  name: example-app
        +  labels:
        +    app: apple
        +spec:
        +  containers:
        +    - name: example-app
        +      image: hashicorp/http-echo
        +      args:
        +        - "-text=apple"
        +---
        +kind: Service
        +apiVersion: v1
        +metadata:
        +  name: example-service
        +spec:
        +  selector:
        +    app: apple
        +  ports:
        +    - port: 5678 # Default port for image
        +---
        +kind: Pod
        +apiVersion: v1
        +metadata:
        +  name: example-app1
        +  labels:
        +    app: apple1
        +spec:
        +  containers:
        +    - name: example-app1
        +      image: hashicorp/http-echo
        +      args:
        +        - "-text=apple"
        +---
        +apiVersion: extensions/v1beta1
        +kind: Service
        +apiVersion: v1
        +metadata:
        +  name: example-service1
        +spec:
        +  selector:
        +    app: apple1
        +  ports:
        +    - port: 5679 # Default port for image
        +---
        +apiVersion: networking.k8s.io/v1
        +kind: Ingress
        +metadata:
        +  name: example-ingress
        +  annotations:
        +    ingress.kubernetes.io/rewrite-target: /
        +    ingress.kubernetes.io/scheme: internet-facing
        +    external-dns.alpha.kubernetes.io/hostname: apple.example.com.
        +    external-dns.alpha.kubernetes.io/target: 10.10.10.3,10.10.10.25
        +spec:
        +  rules:
        +  - http:
        +      paths:
        +        - path: /apple
        +          backend:
        +            service:
        +              name: example-service
        +              port:
        +                number: 5678
        +---
        +apiVersion: networking.k8s.io/v1
        +kind: Ingress
        +metadata:
        +  name: example-ingress1
        +  annotations:
        +    ingress.kubernetes.io/rewrite-target: /
        +    ingress.kubernetes.io/scheme: internet-facing
        +    external-dns.alpha.kubernetes.io/hostname: apple-banana.example.com.
        +    external-dns.alpha.kubernetes.io/target: 10.10.10.3
        +spec:
        +  rules:
        +  - http:
        +      paths:
        +        - path: /apple
        +          backend:
        +            service:
        +              name: example-service1
        +              port:
        +                number: 5679
        +
      • +
      +
    • +
    • Then, create the deployment and service:
      +
      $ kubectl create -f apple-banana-echo.yaml
      +$ kubectl create -f external-dns.yaml
      +
    • +
    • Depending on where you run your service from, it can take a few minutes for your cloud provider to create an external IP for the service.
    • +
    • Please verify on the UltraDNS UI, that the records have been created under the zone “example.com”.
    • +
    • Finally, you will need to clean up the deployment and service. Please verify on the UI afterwards that the records have been deleted from the zone “example.com”:
      +console +$ kubectl delete -f apple-banana-echo.yaml +$ kubectl delete -f external-dns.yaml
    • +
    + +
    +
    + + + Last update: + May 29, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/vinyldns/index.html b/v0.13.6/tutorials/vinyldns/index.html new file mode 100644 index 0000000000..a6d6a57ed0 --- /dev/null +++ b/v0.13.6/tutorials/vinyldns/index.html @@ -0,0 +1,2205 @@ + + + + + + + + + + + + + + + + + + Setting up ExternalDNS for VinylDNS - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + + + + +
    +
    + + + + + + + + +

    Setting up ExternalDNS for VinylDNS

    +

    This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using VinylDNS.

    +

    The environment vars VINYLDNS_ACCESS_KEY, VINYLDNS_SECRET_KEY, and VINYLDNS_HOST will be needed to run ExternalDNS with VinylDNS.

    +

    Create a sample deployment and service for external-dns to use

    +

    Run an application and expose it via a Kubernetes Service:

    +
    $ kubectl run nginx --image=nginx --replicas=1 --port=80
    +$ kubectl expose deployment nginx --port=80 --target-port=80 --type=LoadBalancer
    +
    +

    Annotate the Service with your desired external DNS name. Make sure to change example.org to your domain.

    +
    $ kubectl annotate service nginx "external-dns.alpha.kubernetes.io/hostname=nginx.example.org."
    +
    +

    After the service is up and running, it should get an EXTERNAL-IP. At first this may showing as <pending>

    +
    $ kubectl get svc
    +NAME         CLUSTER-IP   EXTERNAL-IP   PORT(S)        AGE
    +kubernetes   10.0.0.1     <none>        443/TCP        1h
    +nginx        10.0.0.115   <pending>     80:30543/TCP   10s
    +
    +

    Once it’s available

    +
    % kubectl get svc
    +NAME         CLUSTER-IP   EXTERNAL-IP   PORT(S)        AGE
    +kubernetes   10.0.0.1     <none>        443/TCP        1h
    +nginx        10.0.0.115   34.x.x.x      80:30543/TCP   2m
    +
    +

    Deploy ExternalDNS to Kubernetes

    +

    Connect your kubectl client to the cluster you want to test ExternalDNS with.
    +Then apply one of the following manifests file to deploy ExternalDNS.

    +

    Note for examples below

    +

    When using registry=txt option, make sure to also use the txt-prefix and txt-owner-id options as well. If you try to create a TXT record in VinylDNS without a prefix, it will try to create a TXT record with the same name as your actual DNS record and fail (creating a stranded record external-dns cannot manage).

    +

    Manifest (for clusters without RBAC enabled)

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --provider=vinyldns
    +        - --source=service
    +        - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
    +        - --registry=txt
    +        - --txt-owner-id=grizz
    +        - --txt-prefix=txt-
    +        env:
    +        - name: VINYLDNS_HOST
    +          value: "YOUR_VINYLDNS_HOST"
    +        - name: VINYLDNS_ACCESS_KEY
    +          value: "YOUR_VINYLDNS_ACCESS_KEY"
    +        - name: VINYLDNS_SECRET_KEY
    +          value: "YOUR_VINYLDNS_SECRET_KEY"
    +
    +

    Manifest (for clusters with RBAC enabled)

    +
    apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRole
    +metadata:
    +  name: external-dns
    +rules:
    +- apiGroups: [""]
    +  resources: ["services","endpoints","pods"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: ["extensions","networking.k8s.io"]
    +  resources: ["ingresses"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: [""]
    +  resources: ["nodes"]
    +  verbs: ["list"]
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRoleBinding
    +metadata:
    +  name: external-dns-viewer
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: ClusterRole
    +  name: external-dns
    +subjects:
    +- kind: ServiceAccount
    +  name: external-dns
    +  namespace: default
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      serviceAccountName: external-dns
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --provider=vinyldns
    +        - --source=service
    +        - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
    +        - --registry=txt
    +        - --txt-owner-id=grizz
    +        - --txt-prefix=txt-
    +        env:
    +        env:
    +        - name: VINYLDNS_HOST
    +          value: "YOUR_VINYLDNS_HOST"
    +        - name: VINYLDNS_ACCESS_KEY
    +          value: "YOUR_VINYLDNS_ACCESS_KEY"
    +        - name: VINYLDNS_SECRET_KEY
    +          value: "YOUR_VINYLDNS_SECRET_KEYY
    +
    +

    Running a locally built version pointed to the above nginx service

    +

    Make sure your kubectl is configured correctly. Assuming you have the sources, build and run it like below.

    +

    The vinyl access details needs to exported to the environment before running.

    +
    make
    +# output skipped
    +
    +export VINYLDNS_HOST=<fqdn of vinyl dns api>
    +export VINYLDNS_ACCESS_KEY=<access key>
    +export VINYLDNS_SECRET_KEY=<secret key>
    +
    +./build/external-dns \
    +    --provider=vinyldns \
    +    --source=service \
    +    --domain-filter=elements.capsps.comcast.net. \
    +    --zone-id-filter=20e8bfd2-3a70-4e1b-8e11-c9c1948528d3 \
    +    --registry=txt \
    +    --txt-owner-id=grizz \
    +    --txt-prefix=txt- \
    +    --namespace=default \
    +    --once \
    +    --dry-run \
    +    --log-level debug
    +
    +INFO[0000] running in dry-run mode. No changes to DNS records will be made.
    +INFO[0000] Created Kubernetes client https://some-k8s-cluster.example.com
    +INFO[0001] Zone: [nginx.example.org.]
    +# output skipped
    +
    +

    Having --dry-run=true and --log-level=debug is a great way to see exactly what VinylDNS is doing or is about to do.

    + +
    +
    + + + Last update: + May 29, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/v0.13.6/tutorials/vultr/index.html b/v0.13.6/tutorials/vultr/index.html new file mode 100644 index 0000000000..4379441caa --- /dev/null +++ b/v0.13.6/tutorials/vultr/index.html @@ -0,0 +1,2240 @@ + + + + + + + + + + + + + + + + + + Setting up ExternalDNS for Services on Vultr - external-dns + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + + + +
    + + + + + + + +
    + +
    + + + + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    +
    + + + + + + +
    +
    + + + + + + + + +

    Setting up ExternalDNS for Services on Vultr

    +

    This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using Vultr DNS.

    +

    Make sure to use >=0.6 version of ExternalDNS for this tutorial.

    +

    Managing DNS with Vultr

    +

    If you want to read up on vultr DNS service you can read the following tutorial:
    +Introduction to Vultr DNS

    +

    Create a new DNS Zone where you want to create your records in. For the examples we will be using example.com

    +

    Creating Vultr Credentials

    +

    You will need to create a new API Key which can be found on the Vultr Dashboard.

    +

    The environment variable VULTR_API_KEY will be needed to run ExternalDNS with Vultr.

    +

    Deploy ExternalDNS

    +

    Connect your kubectl client to the cluster you want to test ExternalDNS with.
    +Then apply one of the following manifests file to deploy ExternalDNS.

    +

    Manifest (for clusters without RBAC enabled)

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service # ingress is also possible
    +        - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
    +        - --provider=vultr
    +        env:
    +        - name: VULTR_API_KEY
    +          value: "YOU_VULTR_API_KEY"
    +
    +

    Manifest (for clusters with RBAC enabled)

    +
    apiVersion: v1
    +kind: ServiceAccount
    +metadata:
    +  name: external-dns
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRole
    +metadata:
    +  name: external-dns
    +rules:
    +- apiGroups: [""]
    +  resources: ["services","endpoints","pods"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: ["extensions","networking.k8s.io"]
    +  resources: ["ingresses"]
    +  verbs: ["get","watch","list"]
    +- apiGroups: [""]
    +  resources: ["nodes"]
    +  verbs: ["list"]
    +---
    +apiVersion: rbac.authorization.k8s.io/v1
    +kind: ClusterRoleBinding
    +metadata:
    +  name: external-dns-viewer
    +roleRef:
    +  apiGroup: rbac.authorization.k8s.io
    +  kind: ClusterRole
    +  name: external-dns
    +subjects:
    +- kind: ServiceAccount
    +  name: external-dns
    +  namespace: default
    +---
    +apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: external-dns
    +spec:
    +  strategy:
    +    type: Recreate
    +  selector:
    +    matchLabels:
    +      app: external-dns
    +  template:
    +    metadata:
    +      labels:
    +        app: external-dns
    +    spec:
    +      serviceAccountName: external-dns
    +      containers:
    +      - name: external-dns
    +        image: registry.k8s.io/external-dns/external-dns:v0.13.5
    +        args:
    +        - --source=service # ingress is also possible
    +        - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
    +        - --provider=vultr
    +        env:
    +        - name: VULTR_API_KEY
    +          value: "YOU_VULTR_API_KEY"
    +
    +

    Deploying a Nginx Service

    +

    Create a service file called ‘nginx.yaml’ with the following contents:

    +
    apiVersion: apps/v1
    +kind: Deployment
    +metadata:
    +  name: nginx
    +spec:
    +  selector:
    +    matchLabels:
    +      app: nginx
    +  template:
    +    metadata:
    +      labels:
    +        app: nginx
    +    spec:
    +      containers:
    +      - image: nginx
    +        name: nginx
    +        ports:
    +        - containerPort: 80
    +---
    +apiVersion: v1
    +kind: Service
    +metadata:
    +  name: nginx
    +  annotations:
    +    external-dns.alpha.kubernetes.io/hostname: my-app.example.com
    +spec:
    +  selector:
    +    app: nginx
    +  type: LoadBalancer
    +  ports:
    +    - protocol: TCP
    +      port: 80
    +      targetPort: 80
    +
    +

    Note the annotation on the service; use the same hostname as the Vultr DNS zone created above.

    +

    ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records.

    +

    Create the deployment and service:

    +
    $ kubectl create -f nginx.yaml
    +
    +

    Depending where you run your service it can take a little while for your cloud provider to create an external IP for the service.

    +

    Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize the Vultr DNS records.

    +

    Verifying Vultr DNS records

    +

    Check your Vultr UI to view the records for your Vultr DNS zone.

    +

    Click on the zone for the one created above if a different domain was used.

    +

    This should show the external IP address of the service as the A record for your domain.

    +

    Cleanup

    +

    Now that we have verified that ExternalDNS will automatically manage Vultr DNS records, we can delete the tutorial’s example:

    +
    $ kubectl delete service -f nginx.yaml
    +$ kubectl delete service -f externaldns.yaml
    +
    + +
    +
    + + + Last update: + May 29, 2023 + + + +
    + + +
    +
    +
    + + + + Back to top + + +
    + + + +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/versions.json b/versions.json index 1079d36bee..86d0891f29 100644 --- a/versions.json +++ b/versions.json @@ -1 +1 @@ -[{"version": "v0.13.5", "title": "v0.13.5", "aliases": ["latest"]}, {"version": "v0.13.4", "title": "v0.13.4", "aliases": []}, {"version": "v0.13.3", "title": "v0.13.3", "aliases": []}, {"version": "v0.13.2", "title": "v0.13.2", "aliases": []}, {"version": "v0.13.1", "title": "v0.13.1", "aliases": []}, {"version": "v0.13.0", "title": "v0.13.0", "aliases": []}, {"version": "v0.12.2", "title": "v0.12.2", "aliases": []}, {"version": "v0.12.1", "title": "v0.12.1", "aliases": []}, {"version": "v0.12.0", "title": "v0.12.0", "aliases": []}] \ No newline at end of file +[{"version": "v0.13.6", "title": "v0.13.6", "aliases": ["latest"]}, {"version": "v0.13.5", "title": "v0.13.5", "aliases": []}, {"version": "v0.13.4", "title": "v0.13.4", "aliases": []}, {"version": "v0.13.3", "title": "v0.13.3", "aliases": []}, {"version": "v0.13.2", "title": "v0.13.2", "aliases": []}, {"version": "v0.13.1", "title": "v0.13.1", "aliases": []}, {"version": "v0.13.0", "title": "v0.13.0", "aliases": []}, {"version": "v0.12.2", "title": "v0.12.2", "aliases": []}, {"version": "v0.12.1", "title": "v0.12.1", "aliases": []}, {"version": "v0.12.0", "title": "v0.12.0", "aliases": []}] \ No newline at end of file