Merge branch 'master' into master

This commit is contained in:
Kushal Bhandari 2020-06-30 11:18:58 -07:00 committed by GitHub
commit 3b085c5fcc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 891 additions and 201 deletions

View File

@ -39,6 +39,7 @@ Providers
- [ ] Azure
- [ ] Cloudflare
- [x] DigitalOcean
- [x] DNSimple
- [x] Google
- [ ] InMemory
- [x] Linode
@ -61,6 +62,9 @@ This value is a constant in the provider code.
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.

View File

@ -139,12 +139,12 @@ The following are relevant snippets from that tutorial.
#### Install a sample service
With automatic sidecar injection:
```bash
$ kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.0/samples/httpbin/httpbin.yaml
$ kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.6/samples/httpbin/httpbin.yaml
```
Otherwise:
```bash
$ kubectl apply -f <(istioctl kube-inject -f https://raw.githubusercontent.com/istio/istio/release-1.0/samples/httpbin/httpbin.yaml)
$ kubectl apply -f <(istioctl kube-inject -f https://raw.githubusercontent.com/istio/istio/release-1.6/samples/httpbin/httpbin.yaml)
```
#### Create an Istio Gateway:

9
go.mod
View File

@ -20,13 +20,14 @@ require (
github.com/cloudflare/cloudflare-go v0.10.1
github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381
github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba
github.com/digitalocean/godo v1.34.0
github.com/digitalocean/godo v1.36.0
github.com/dnsimple/dnsimple-go v0.60.0
github.com/exoscale/egoscale v0.18.1
github.com/fatih/structs v1.1.0 // indirect
github.com/ffledgling/pdns-go v0.0.0-20180219074714-524e7daccd99
github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b // indirect
github.com/golang/sync v0.0.0-20180314180146-1d60e4601c6f
github.com/google/go-cmp v0.4.1
github.com/gophercloud/gophercloud v0.1.0
github.com/gorilla/mux v1.7.4 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
@ -57,11 +58,11 @@ require (
github.com/transip/gotransip v5.8.2+incompatible
github.com/ultradns/ultradns-sdk-go v0.0.0-20200616202852-e62052662f60
github.com/vinyldns/go-vinyldns v0.0.0-20190611170422-7119fe55ed92
github.com/vultr/govultr v0.3.2
github.com/vultr/govultr v0.4.2
go.etcd.io/etcd v0.5.0-alpha.5.0.20200401174654-e694b7bb0875
go.uber.org/ratelimit v0.1.0
golang.org/x/net v0.0.0-20200202094626-16171245cfb2
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
google.golang.org/api v0.15.0
gopkg.in/ns1/ns1-go.v2 v2.0.0-20190322154155-0dafb5275fd1
gopkg.in/yaml.v2 v2.2.8

25
go.sum
View File

@ -108,7 +108,6 @@ github.com/cloudflare/cloudflare-go v0.10.1 h1:d2CL6F9k2O0Ux0w27LgogJ5UOzZRj6a/h
github.com/cloudflare/cloudflare-go v0.10.1/go.mod h1:C0Y6eWnTJPMK2ceuOxx2pjh78UUHihcXeTTHb8r7QjU=
github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381 h1:rdRS5BT13Iae9ssvcslol66gfOOXjaLYwqerEn/cl9s=
github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381/go.mod h1:e5+USP2j8Le2M0Jo3qKPFnNhuo1wueU4nWHCXBOfQ14=
github.com/cncf/udpa/go v0.0.0-20200313221541-5f7e5dd04533 h1:8wZizuKuZVu5COB7EsBYxBQz8nRcXXn5d4Gt91eJLvU=
github.com/cncf/udpa/go v0.0.0-20200313221541-5f7e5dd04533/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa h1:OaNxuTZr7kxeODyLWsRMC+OD03aFUH+mW6r2d+MWa5Y=
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
@ -140,8 +139,8 @@ github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8l
github.com/dgrijalva/jwt-go v0.0.0-20160705203006-01aeca54ebda/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/digitalocean/godo v1.34.0 h1:OXJhLLJS2VTB5SziTyCq8valKVZ0uBHCFQsDW3/HF78=
github.com/digitalocean/godo v1.34.0/go.mod h1:gfLm3JSupWD9V/ibQygXWW3IVz7hranzckH5UimhZsI=
github.com/digitalocean/godo v1.36.0 h1:eRF8wNzHZyU7/wI3De/MQgiVSWdseDaf27bXj2gnOO0=
github.com/digitalocean/godo v1.36.0/go.mod h1:p7dOjjtSBqCTUksqtA5Fd3uaKs9kyTq2xcz76ulEJRU=
github.com/dimchansky/utfbom v1.1.0 h1:FcM3g+nofKgUteL8dm/UpdRXNC9KmADgTpLKsu0TRo4=
github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
github.com/dnaeon/go-vcr v1.0.1 h1:r8L/HqC0Hje5AXMu1ooW8oyQyOFv4GxqpL0nRP7SLLY=
@ -163,7 +162,6 @@ github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT
github.com/envoyproxy/go-control-plane v0.8.2/go.mod h1:EWRTAFN6uuDZIa6KOuUfrOMJ7ySgXZ44rVKiTWjKe34=
github.com/envoyproxy/go-control-plane v0.9.0 h1:67WMNTvGrl7V1dWdKCeTwxDr7nio9clKoTlLhwIPnT4=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.5 h1:lRJIqDD8yjV1YyPRqecMdytjDLs2fTXq363aCib5xPU=
github.com/envoyproxy/go-control-plane v0.9.5/go.mod h1:OXl5to++W0ctG+EHWTFUjiypVxC/Y4VLc/KFU+al13s=
github.com/envoyproxy/protoc-gen-validate v0.0.0-20190405222122-d6164de49109/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v0.0.14/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
@ -273,6 +271,8 @@ github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/sync v0.0.0-20180314180146-1d60e4601c6f h1:kSqKc8ouCLIBHqdj9a9xxhtxlZhNqbePClixA4HoM44=
github.com/golang/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:YCHYtYb9c8Q7XgYVYjmJBPtFPKx5QvOcPxHZWjldabE=
github.com/google/btree v0.0.0-20160524151835-7d79101e329e/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
@ -286,6 +286,8 @@ github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-github v15.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/go-cmp v0.4.1 h1:/exdXoGamhu5ONeUJH0deniYLWYvQwW66yvlfiiKTu0=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
@ -336,8 +338,8 @@ github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVo
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-retryablehttp v0.6.4 h1:BbgctKO892xEyOXnGiaAwIoSq1QZ/SS4AhjoAh9DnfY=
github.com/hashicorp/go-retryablehttp v0.6.4/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
github.com/hashicorp/go-retryablehttp v0.6.6 h1:HJunrbHTDDbBb/ay4kxa1n+dLmttUlnP3V9oNE4hmsM=
github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.3 h1:YPkqC67at8FYaadspW/6uE0COsBxS2656RLEr8Bppgk=
@ -600,8 +602,8 @@ github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijb
github.com/urfave/cli v1.21.0/go.mod h1:lxDj6qX9Q6lWQxIrbrT0nwecwUtRnhVZAJjJZrVUZZQ=
github.com/vinyldns/go-vinyldns v0.0.0-20190611170422-7119fe55ed92 h1:Q76MzqJu++vAfhj0mVf7t0F4xHUbg+V/d/Uk5PBQjRU=
github.com/vinyldns/go-vinyldns v0.0.0-20190611170422-7119fe55ed92/go.mod h1:AZuEfReFWdvtU0LatbLpo70t3lqdLvph2D5mqFP0bkA=
github.com/vultr/govultr v0.3.2 h1:1tV/88jkm+4Y345qAXBe3peNbnmvCY/VAIZApklbKkI=
github.com/vultr/govultr v0.3.2/go.mod h1:81RwK1wAmb08alkFDJiZmu9gdv+IO+UamzaF0+PIieE=
github.com/vultr/govultr v0.4.2 h1:9i8xKZ+xp6vwZ9raqHoBLzhB4wCnMj7nOQTj5YIRLWY=
github.com/vultr/govultr v0.4.2/go.mod h1:TUuUizMOFc7z+PNMssb6iGjKjQfpw5arIaOLfocVudQ=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
@ -703,12 +705,15 @@ golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8ou
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190130055435-99b60b757ec1/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -750,6 +755,8 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191105231009-c1f44814a5cd h1:3x5uuvBgE6oaXJjCOvpCC1IpgJogqQ+PqGGU3ZxAgII=
golang.org/x/sys v0.0.0-20191105231009-c1f44814a5cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20171227012246-e19ae1496984/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View File

@ -15,3 +15,6 @@ rules:
- apiGroups: [""]
resources: ["nodes"]
verbs: ["list"]
- apiGroups: [""]
resources: ["endpoints"]
verbs: ["get","watch","list"]

View File

@ -112,7 +112,6 @@ func main() {
KubeConfig: cfg.KubeConfig,
KubeMaster: cfg.Master,
ServiceTypeFilter: cfg.ServiceTypeFilter,
IstioIngressGatewayServices: cfg.IstioIngressGatewayServices,
CFAPIEndpoint: cfg.CFAPIEndpoint,
CFUsername: cfg.CFUsername,
CFPassword: cfg.CFPassword,

View File

@ -41,7 +41,6 @@ type Config struct {
Master string
KubeConfig string
RequestTimeout time.Duration
IstioIngressGatewayServices []string
ContourLoadBalancerService string
SkipperRouteGroupVersion string
Sources []string
@ -148,7 +147,6 @@ var defaultConfig = &Config{
Master: "",
KubeConfig: "",
RequestTimeout: time.Second * 30,
IstioIngressGatewayServices: []string{"istio-system/istio-ingressgateway"},
ContourLoadBalancerService: "heptio-contour/contour",
SkipperRouteGroupVersion: "zalando.org/v1",
Sources: nil,

View File

@ -32,13 +32,6 @@ import (
)
const (
// DigitalOceanCreate is a ChangeAction enum value
DigitalOceanCreate = "CREATE"
// DigitalOceanDelete is a ChangeAction enum value
DigitalOceanDelete = "DELETE"
// DigitalOceanUpdate is a ChangeAction enum value
DigitalOceanUpdate = "UPDATE"
// digitalOceanRecordTTL is the default TTL value
digitalOceanRecordTTL = 300
)
@ -54,10 +47,31 @@ type DigitalOceanProvider struct {
DryRun bool
}
// DigitalOceanChange differentiates between ChangActions
type DigitalOceanChange struct {
Action string
ResourceRecordSet godo.DomainRecord
type digitalOceanChangeCreate struct {
Domain string
Options *godo.DomainRecordEditRequest
}
type digitalOceanChangeUpdate struct {
Domain string
DomainRecord godo.DomainRecord
Options *godo.DomainRecordEditRequest
}
type digitalOceanChangeDelete struct {
Domain string
RecordID int
}
// DigitalOceanChange contains all changes to apply to DNS
type digitalOceanChanges struct {
Creates []*digitalOceanChangeCreate
Updates []*digitalOceanChangeUpdate
Deletes []*digitalOceanChangeDelete
}
func (c *digitalOceanChanges) Empty() bool {
return len(c.Creates) == 0 && len(c.Updates) == 0 && len(c.Deletes) == 0
}
// NewDigitalOceanProvider initializes a new DigitalOcean DNS based Provider.
@ -71,13 +85,13 @@ func NewDigitalOceanProvider(ctx context.Context, domainFilter endpoint.DomainFi
}))
client := godo.NewClient(oauthClient)
provider := &DigitalOceanProvider{
p := &DigitalOceanProvider{
Client: client.Domains,
domainFilter: domainFilter,
apiPageSize: apiPageSize,
DryRun: dryRun,
}
return provider, nil
return p, nil
}
// Zones returns the list of hosted zones.
@ -98,12 +112,45 @@ func (p *DigitalOceanProvider) Zones(ctx context.Context) ([]godo.Domain, error)
return result, nil
}
// Merge Endpoints with the same Name and Type into a single endpoint with multiple Targets.
func mergeEndpointsByNameType(endpoints []*endpoint.Endpoint) []*endpoint.Endpoint {
endpointsByNameType := map[string][]*endpoint.Endpoint{}
for _, e := range endpoints {
key := fmt.Sprintf("%s-%s", e.DNSName, e.RecordType)
endpointsByNameType[key] = append(endpointsByNameType[key], e)
}
// If no merge occurred, just return the existing endpoints.
if len(endpointsByNameType) == len(endpoints) {
return endpoints
}
// Otherwise, construct a new list of endpoints with the endpoints merged.
var result []*endpoint.Endpoint
for _, endpoints := range endpointsByNameType {
dnsName := endpoints[0].DNSName
recordType := endpoints[0].RecordType
targets := make([]string, len(endpoints))
for i, e := range endpoints {
targets[i] = e.Targets[0]
}
e := endpoint.NewEndpoint(dnsName, recordType, targets...)
result = append(result, e)
}
return result
}
// Records returns the list of records in a given zone.
func (p *DigitalOceanProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
zones, err := p.Zones(ctx)
if err != nil {
return nil, err
}
endpoints := []*endpoint.Endpoint{}
for _, zone := range zones {
records, err := p.fetchRecords(ctx, zone.Name)
@ -121,11 +168,21 @@ func (p *DigitalOceanProvider) Records(ctx context.Context) ([]*endpoint.Endpoin
name = zone.Name
}
endpoints = append(endpoints, endpoint.NewEndpoint(name, r.Type, r.Data))
ep := endpoint.NewEndpoint(name, r.Type, r.Data)
endpoints = append(endpoints, ep)
}
}
}
// Merge endpoints with the same name and type (e.g., multiple A records for a single
// DNS name) into one endpoint with multiple targets.
endpoints = mergeEndpointsByNameType(endpoints)
// Log the endpoints that were found.
log.WithFields(log.Fields{
"endpoints": endpoints,
}).Debug("Endpoints generated from DigitalOcean DNS")
return endpoints, nil
}
@ -179,160 +236,379 @@ func (p *DigitalOceanProvider) fetchZones(ctx context.Context) ([]godo.Domain, e
return allZones, nil
}
// submitChanges takes a zone and a collection of Changes and sends them as a single transaction.
func (p *DigitalOceanProvider) submitChanges(ctx context.Context, changes []*DigitalOceanChange) error {
func (p *DigitalOceanProvider) getRecordsByDomain(ctx context.Context) (map[string][]godo.DomainRecord, provider.ZoneIDName, error) {
recordsByDomain := map[string][]godo.DomainRecord{}
zones, err := p.Zones(ctx)
if err != nil {
return nil, nil, err
}
zonesByDomain := make(map[string]godo.Domain)
zoneNameIDMapper := provider.ZoneIDName{}
for _, z := range zones {
zoneNameIDMapper.Add(z.Name, z.Name)
zonesByDomain[z.Name] = z
}
// Fetch records for each zone
for _, zone := range zones {
records, err := p.fetchRecords(ctx, zone.Name)
if err != nil {
return nil, nil, err
}
recordsByDomain[zone.Name] = append(recordsByDomain[zone.Name], records...)
}
return recordsByDomain, zoneNameIDMapper, nil
}
// Make a DomainRecordEditRequest that conforms to DigitalOcean API requirements:
// - Records at root of the zone have `@` as the name
// - CNAME records must end in a `.`
func makeDomainEditRequest(domain, name, recordType, data string, ttl int) *godo.DomainRecordEditRequest {
// Trim the domain off the name if present.
adjustedName := strings.TrimSuffix(name, "."+domain)
// Record at the root should be defined as @ instead of the full domain name.
if adjustedName == domain {
adjustedName = "@"
}
// For some reason the DO API requires the '.' at the end of "data" in case of CNAME request.
// Example: {"type":"CNAME","name":"hello","data":"www.example.com."}
if recordType == endpoint.RecordTypeCNAME && !strings.HasSuffix(data, ".") {
data += "."
}
return &godo.DomainRecordEditRequest{
Name: adjustedName,
Type: recordType,
Data: data,
TTL: ttl,
}
}
// submitChanges applies an instance of `digitalOceanChanges` to the DigitalOcean API.
func (p *DigitalOceanProvider) submitChanges(ctx context.Context, changes *digitalOceanChanges) error {
// return early if there is nothing to change
if len(changes) == 0 {
if changes.Empty() {
return nil
}
zones, err := p.Zones(ctx)
for _, c := range changes.Creates {
log.WithFields(log.Fields{
"domain": c.Domain,
"dnsName": c.Options.Name,
"recordType": c.Options.Type,
"data": c.Options.Data,
"ttl": c.Options.TTL,
}).Debug("Creating domain record")
if p.DryRun {
continue
}
_, _, err := p.Client.CreateRecord(ctx, c.Domain, c.Options)
if err != nil {
return err
}
}
for _, u := range changes.Updates {
log.WithFields(log.Fields{
"domain": u.Domain,
"dnsName": u.Options.Name,
"recordType": u.Options.Type,
"data": u.Options.Data,
"ttl": u.Options.TTL,
}).Debug("Updating domain record")
if p.DryRun {
continue
}
_, _, err := p.Client.EditRecord(ctx, u.Domain, u.DomainRecord.ID, u.Options)
if err != nil {
return err
}
}
for _, d := range changes.Deletes {
log.WithFields(log.Fields{
"domain": d.Domain,
"recordId": d.RecordID,
}).Debug("Deleting domain record")
if p.DryRun {
continue
}
_, err := p.Client.DeleteRecord(ctx, d.Domain, d.RecordID)
if err != nil {
return err
}
}
return nil
}
func getTTLFromEndpoint(ep *endpoint.Endpoint) int {
if ep.RecordTTL.IsConfigured() {
return int(ep.RecordTTL)
}
return digitalOceanRecordTTL
}
func endpointsByZone(zoneNameIDMapper provider.ZoneIDName, endpoints []*endpoint.Endpoint) map[string][]*endpoint.Endpoint {
endpointsByZone := make(map[string][]*endpoint.Endpoint)
for _, ep := range endpoints {
zoneID, _ := zoneNameIDMapper.FindZone(ep.DNSName)
if zoneID == "" {
log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected", ep.DNSName)
continue
}
endpointsByZone[zoneID] = append(endpointsByZone[zoneID], ep)
}
return endpointsByZone
}
func getMatchingDomainRecords(records []godo.DomainRecord, domain string, ep *endpoint.Endpoint) []godo.DomainRecord {
var name string
if ep.DNSName != domain {
name = strings.TrimSuffix(ep.DNSName, "."+domain)
} else {
name = "@"
}
var result []godo.DomainRecord
for _, r := range records {
if r.Name == name && r.Type == ep.RecordType {
result = append(result, r)
}
}
return result
}
func processCreateActions(
recordsByDomain map[string][]godo.DomainRecord,
createsByDomain map[string][]*endpoint.Endpoint,
changes *digitalOceanChanges,
) error {
// Process endpoints that need to be created.
for domain, endpoints := range createsByDomain {
if len(endpoints) == 0 {
log.WithFields(log.Fields{
"domain": domain,
}).Debug("Skipping domain, no creates found.")
continue
}
records := recordsByDomain[domain]
for _, ep := range endpoints {
// Warn if there are existing records since we expect to create only new records.
matchingRecords := getMatchingDomainRecords(records, domain, ep)
if len(matchingRecords) > 0 {
log.WithFields(log.Fields{
"domain": domain,
"dnsName": ep.DNSName,
"recordType": ep.RecordType,
}).Warn("Preexisting records exist which should not exist for creation actions.")
}
ttl := getTTLFromEndpoint(ep)
for _, target := range ep.Targets {
changes.Creates = append(changes.Creates, &digitalOceanChangeCreate{
Domain: domain,
Options: makeDomainEditRequest(domain, ep.DNSName, ep.RecordType, target, ttl),
})
}
}
}
return nil
}
func processUpdateActions(
recordsByDomain map[string][]godo.DomainRecord,
updatesByDomain map[string][]*endpoint.Endpoint,
changes *digitalOceanChanges,
) error {
// Generate creates and updates based on existing
for domain, updates := range updatesByDomain {
if len(updates) == 0 {
log.WithFields(log.Fields{
"domain": domain,
}).Debug("Skipping Zone, no updates found.")
continue
}
records := recordsByDomain[domain]
log.WithFields(log.Fields{
"domain": domain,
"records": records,
}).Debug("Records for domain")
for _, ep := range updates {
matchingRecords := getMatchingDomainRecords(records, domain, ep)
log.WithFields(log.Fields{
"endpoint": ep,
"matchingRecords": matchingRecords,
}).Debug("matching records")
if len(matchingRecords) == 0 {
log.WithFields(log.Fields{
"domain": domain,
"dnsName": ep.DNSName,
"recordType": ep.RecordType,
}).Warn("Planning an update but no existing records found.")
}
matchingRecordsByTarget := map[string]godo.DomainRecord{}
for _, r := range matchingRecords {
matchingRecordsByTarget[r.Data] = r
}
ttl := getTTLFromEndpoint(ep)
// Generate create and delete actions based on existence of a record for each target.
for _, target := range ep.Targets {
if record, ok := matchingRecordsByTarget[target]; ok {
log.WithFields(log.Fields{
"domain": domain,
"dnsName": ep.DNSName,
"recordType": ep.RecordType,
"target": target,
}).Warn("Updating existing target")
changes.Updates = append(changes.Updates, &digitalOceanChangeUpdate{
Domain: domain,
DomainRecord: record,
Options: makeDomainEditRequest(domain, ep.DNSName, ep.RecordType, target, ttl),
})
delete(matchingRecordsByTarget, target)
} else {
// Record did not previously exist, create new 'target'
log.WithFields(log.Fields{
"domain": domain,
"dnsName": ep.DNSName,
"recordType": ep.RecordType,
"target": target,
}).Warn("Creating new target")
changes.Creates = append(changes.Creates, &digitalOceanChangeCreate{
Domain: domain,
Options: makeDomainEditRequest(domain, ep.DNSName, ep.RecordType, target, ttl),
})
}
}
// Any remaining records have been removed, delete them
for _, record := range matchingRecordsByTarget {
log.WithFields(log.Fields{
"domain": domain,
"dnsName": ep.DNSName,
"recordType": ep.RecordType,
"target": record.Data,
}).Warn("Deleting target")
changes.Deletes = append(changes.Deletes, &digitalOceanChangeDelete{
Domain: domain,
RecordID: record.ID,
})
}
}
}
return nil
}
func processDeleteActions(
recordsByDomain map[string][]godo.DomainRecord,
deletesByDomain map[string][]*endpoint.Endpoint,
changes *digitalOceanChanges,
) error {
// Generate delete actions for each deleted endpoint.
for domain, deletes := range deletesByDomain {
if len(deletes) == 0 {
log.WithFields(log.Fields{
"domain": domain,
}).Debug("Skipping Zone, no deletes found.")
continue
}
records := recordsByDomain[domain]
for _, ep := range deletes {
matchingRecords := getMatchingDomainRecords(records, domain, ep)
if len(matchingRecords) == 0 {
log.WithFields(log.Fields{
"domain": domain,
"dnsName": ep.DNSName,
"recordType": ep.RecordType,
}).Warn("Records to delete not found.")
}
for _, record := range matchingRecords {
doDelete := false
for _, t := range ep.Targets {
v1 := t
v2 := record.Data
if ep.RecordType == endpoint.RecordTypeCNAME {
v1 = strings.TrimSuffix(t, ".")
v2 = strings.TrimSuffix(t, ".")
}
if v1 == v2 {
doDelete = true
}
}
if doDelete {
changes.Deletes = append(changes.Deletes, &digitalOceanChangeDelete{
Domain: domain,
RecordID: record.ID,
})
}
}
}
}
return nil
}
// ApplyChanges applies the given set of generic changes to the provider.
func (p *DigitalOceanProvider) ApplyChanges(ctx context.Context, planChanges *plan.Changes) error {
// TODO: This should only retrieve zones affected by the given `planChanges`.
recordsByDomain, zoneNameIDMapper, err := p.getRecordsByDomain(ctx)
if err != nil {
return err
}
// separate into per-zone change sets to be passed to the API.
changesByZone := digitalOceanChangesByZone(zones, changes)
for zoneName, changes := range changesByZone {
records, err := p.fetchRecords(ctx, zoneName)
if err != nil {
log.Errorf("Failed to list records in the zone: %s", zoneName)
continue
}
for _, change := range changes {
logFields := log.Fields{
"record": change.ResourceRecordSet.Name,
"type": change.ResourceRecordSet.Type,
"ttl": change.ResourceRecordSet.TTL,
"action": change.Action,
"zone": zoneName,
}
createsByDomain := endpointsByZone(zoneNameIDMapper, planChanges.Create)
updatesByDomain := endpointsByZone(zoneNameIDMapper, planChanges.UpdateNew)
deletesByDomain := endpointsByZone(zoneNameIDMapper, planChanges.Delete)
log.WithFields(logFields).Info("Changing record.")
var changes digitalOceanChanges
if p.DryRun {
continue
}
change.ResourceRecordSet.Name = strings.TrimSuffix(change.ResourceRecordSet.Name, "."+zoneName)
// record at the root should be defined as @ instead of
// the full domain name
if change.ResourceRecordSet.Name == zoneName {
change.ResourceRecordSet.Name = "@"
}
// for some reason the DO API requires the '.' at the end of "data" in case of CNAME request
// Example: {"type":"CNAME","name":"hello","data":"www.example.com."}
if change.ResourceRecordSet.Type == endpoint.RecordTypeCNAME {
change.ResourceRecordSet.Data += "."
}
switch change.Action {
case DigitalOceanCreate:
_, _, err = p.Client.CreateRecord(ctx, zoneName,
&godo.DomainRecordEditRequest{
Data: change.ResourceRecordSet.Data,
Name: change.ResourceRecordSet.Name,
Type: change.ResourceRecordSet.Type,
TTL: change.ResourceRecordSet.TTL,
})
if err != nil {
return err
}
case DigitalOceanDelete:
recordID := p.getRecordID(records, change.ResourceRecordSet)
_, err = p.Client.DeleteRecord(ctx, zoneName, recordID)
if err != nil {
return err
}
case DigitalOceanUpdate:
recordID := p.getRecordID(records, change.ResourceRecordSet)
_, _, err = p.Client.EditRecord(ctx, zoneName, recordID,
&godo.DomainRecordEditRequest{
Data: change.ResourceRecordSet.Data,
Name: change.ResourceRecordSet.Name,
Type: change.ResourceRecordSet.Type,
TTL: change.ResourceRecordSet.TTL,
})
if err != nil {
return err
}
}
}
if err := processCreateActions(recordsByDomain, createsByDomain, &changes); err != nil {
return err
}
return nil
}
// ApplyChanges applies a given set of changes in a given zone.
func (p *DigitalOceanProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
combinedChanges := make([]*DigitalOceanChange, 0, len(changes.Create)+len(changes.UpdateNew)+len(changes.Delete))
combinedChanges = append(combinedChanges, newDigitalOceanChanges(DigitalOceanCreate, changes.Create)...)
combinedChanges = append(combinedChanges, newDigitalOceanChanges(DigitalOceanUpdate, changes.UpdateNew)...)
combinedChanges = append(combinedChanges, newDigitalOceanChanges(DigitalOceanDelete, changes.Delete)...)
return p.submitChanges(ctx, combinedChanges)
}
// newDigitalOceanChanges returns a collection of Changes based on the given records and action.
func newDigitalOceanChanges(action string, endpoints []*endpoint.Endpoint) []*DigitalOceanChange {
changes := make([]*DigitalOceanChange, 0, len(endpoints))
for _, endpoint := range endpoints {
changes = append(changes, newDigitalOceanChange(action, endpoint))
}
return changes
}
func newDigitalOceanChange(action string, endpoint *endpoint.Endpoint) *DigitalOceanChange {
// no annotation results in a TTL of 0, default to 300 for consistency with other providers
var ttl = digitalOceanRecordTTL
if endpoint.RecordTTL.IsConfigured() {
ttl = int(endpoint.RecordTTL)
}
change := &DigitalOceanChange{
Action: action,
ResourceRecordSet: godo.DomainRecord{
Name: endpoint.DNSName,
Type: endpoint.RecordType,
Data: endpoint.Targets[0],
TTL: ttl,
},
}
return change
}
// getRecordID returns the ID from a record.
// the ID is mandatory to update and delete records
func (p *DigitalOceanProvider) getRecordID(records []godo.DomainRecord, record godo.DomainRecord) int {
for _, zoneRecord := range records {
if zoneRecord.Name == record.Name && zoneRecord.Type == record.Type {
return zoneRecord.ID
}
}
return 0
}
// digitalOceanchangesByZone separates a multi-zone change into a single change per zone.
func digitalOceanChangesByZone(zones []godo.Domain, changeSet []*DigitalOceanChange) map[string][]*DigitalOceanChange {
changes := make(map[string][]*DigitalOceanChange)
zoneNameIDMapper := provider.ZoneIDName{}
for _, z := range zones {
zoneNameIDMapper.Add(z.Name, z.Name)
changes[z.Name] = []*DigitalOceanChange{}
}
for _, c := range changeSet {
zone, _ := zoneNameIDMapper.FindZone(c.ResourceRecordSet.Name)
if zone == "" {
log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected", c.ResourceRecordSet.Name)
continue
}
changes[zone] = append(changes[zone], c)
}
return changes
if err := processUpdateActions(recordsByDomain, updatesByDomain, &changes); err != nil {
return err
}
if err := processDeleteActions(recordsByDomain, deletesByDomain, &changes); err != nil {
return err
}
return p.submitChanges(ctx, &changes)
}

View File

@ -20,9 +20,12 @@ import (
"context"
"fmt"
"os"
"reflect"
"sort"
"testing"
"github.com/digitalocean/godo"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -143,10 +146,111 @@ func (m *mockDigitalOceanRecordsFail) Records(ctx context.Context, domain string
return []godo.DomainRecord{}, nil, fmt.Errorf("Failed to get records")
}
func TestNewDigitalOceanChanges(t *testing.T) {
action := DigitalOceanCreate
endpoints := []*endpoint.Endpoint{{DNSName: "new", Targets: endpoint.Targets{"target"}}}
_ = newDigitalOceanChanges(action, endpoints)
func isEmpty(xs interface{}) bool {
if xs != nil {
objValue := reflect.ValueOf(xs)
return objValue.Len() == 0
}
return true
}
// This function is an adapted copy of the testify package's ElementsMatch function with the
// call to ObjectsAreEqual replaced with cmp.Equal which better handles struct's with pointers to
// other structs. It also ignores ordering when comparing unlike cmp.Equal.
func elementsMatch(t *testing.T, listA, listB interface{}, msgAndArgs ...interface{}) (ok bool) {
if listA == nil && listB == nil {
return true
} else if listA == nil {
return isEmpty(listB)
} else if listB == nil {
return isEmpty(listA)
}
aKind := reflect.TypeOf(listA).Kind()
bKind := reflect.TypeOf(listB).Kind()
if aKind != reflect.Array && aKind != reflect.Slice {
return assert.Fail(t, fmt.Sprintf("%q has an unsupported type %s", listA, aKind), msgAndArgs...)
}
if bKind != reflect.Array && bKind != reflect.Slice {
return assert.Fail(t, fmt.Sprintf("%q has an unsupported type %s", listB, bKind), msgAndArgs...)
}
aValue := reflect.ValueOf(listA)
bValue := reflect.ValueOf(listB)
aLen := aValue.Len()
bLen := bValue.Len()
if aLen != bLen {
return assert.Fail(t, fmt.Sprintf("lengths don't match: %d != %d", aLen, bLen), msgAndArgs...)
}
// Mark indexes in bValue that we already used
visited := make([]bool, bLen)
for i := 0; i < aLen; i++ {
element := aValue.Index(i).Interface()
found := false
for j := 0; j < bLen; j++ {
if visited[j] {
continue
}
if cmp.Equal(bValue.Index(j).Interface(), element) {
visited[j] = true
found = true
break
}
}
if !found {
return assert.Fail(t, fmt.Sprintf("element %s appears more times in %s than in %s", element, aValue, bValue), msgAndArgs...)
}
}
return true
}
// Test adapted from test in testify library.
// https://github.com/stretchr/testify/blob/b8f7d52a4a7c581d5ed42333572e7fb857c687c2/assert/assertions_test.go#L768-L796
func TestElementsMatch(t *testing.T) {
mockT := new(testing.T)
cases := []struct {
expected interface{}
actual interface{}
result bool
}{
// matching
{nil, nil, true},
{nil, nil, true},
{[]int{}, []int{}, true},
{[]int{1}, []int{1}, true},
{[]int{1, 1}, []int{1, 1}, true},
{[]int{1, 2}, []int{1, 2}, true},
{[]int{1, 2}, []int{2, 1}, true},
{[2]int{1, 2}, [2]int{2, 1}, true},
{[]string{"hello", "world"}, []string{"world", "hello"}, true},
{[]string{"hello", "hello"}, []string{"hello", "hello"}, true},
{[]string{"hello", "hello", "world"}, []string{"hello", "world", "hello"}, true},
{[3]string{"hello", "hello", "world"}, [3]string{"hello", "world", "hello"}, true},
{[]int{}, nil, true},
// not matching
{[]int{1}, []int{1, 1}, false},
{[]int{1, 2}, []int{2, 2}, false},
{[]string{"hello", "hello"}, []string{"hello"}, false},
}
for _, c := range cases {
t.Run(fmt.Sprintf("ElementsMatch(%#v, %#v)", c.expected, c.actual), func(t *testing.T) {
res := elementsMatch(mockT, c.actual, c.expected)
if res != c.result {
t.Errorf("elementsMatch(%#v, %#v) should return %v", c.actual, c.expected, c.result)
}
})
}
}
func TestDigitalOceanZones(t *testing.T) {
@ -165,6 +269,38 @@ func TestDigitalOceanZones(t *testing.T) {
})
}
func TestDigitalOceanMakeDomainEditRequest(t *testing.T) {
// Ensure that records at the root of the zone get `@` as the name.
r1 := makeDomainEditRequest("example.com", "example.com", endpoint.RecordTypeA,
"1.2.3.4", digitalOceanRecordTTL)
assert.Equal(t, &godo.DomainRecordEditRequest{
Type: endpoint.RecordTypeA,
Name: "@",
Data: "1.2.3.4",
TTL: digitalOceanRecordTTL,
}, r1)
// Ensure the CNAME records have a `.` appended.
r2 := makeDomainEditRequest("example.com", "foo.example.com", endpoint.RecordTypeCNAME,
"bar.example.com", digitalOceanRecordTTL)
assert.Equal(t, &godo.DomainRecordEditRequest{
Type: endpoint.RecordTypeCNAME,
Name: "foo",
Data: "bar.example.com.",
TTL: digitalOceanRecordTTL,
}, r2)
// Ensure that CNAME records do not have an extra `.` appended if they already have a `.`
r3 := makeDomainEditRequest("example.com", "foo.example.com", endpoint.RecordTypeCNAME,
"bar.example.com.", digitalOceanRecordTTL)
assert.Equal(t, &godo.DomainRecordEditRequest{
Type: endpoint.RecordTypeCNAME,
Name: "foo",
Data: "bar.example.com.",
TTL: digitalOceanRecordTTL,
}, r3)
}
func TestDigitalOceanApplyChanges(t *testing.T) {
changes := &plan.Changes{}
provider := &DigitalOceanProvider{
@ -185,6 +321,198 @@ func TestDigitalOceanApplyChanges(t *testing.T) {
}
}
func TestDigitalOceanProcessCreateActions(t *testing.T) {
recordsByDomain := map[string][]godo.DomainRecord{
"example.com": nil,
}
createsByDomain := map[string][]*endpoint.Endpoint{
"example.com": {
endpoint.NewEndpoint("foo.example.com", endpoint.RecordTypeA, "1.2.3.4"),
endpoint.NewEndpoint("example.com", endpoint.RecordTypeCNAME, "foo.example.com"),
},
}
var changes digitalOceanChanges
err := processCreateActions(recordsByDomain, createsByDomain, &changes)
require.NoError(t, err)
assert.Equal(t, 2, len(changes.Creates))
assert.Equal(t, 0, len(changes.Updates))
assert.Equal(t, 0, len(changes.Deletes))
expectedCreates := []*digitalOceanChangeCreate{
{
Domain: "example.com",
Options: &godo.DomainRecordEditRequest{
Name: "foo",
Type: endpoint.RecordTypeA,
Data: "1.2.3.4",
TTL: digitalOceanRecordTTL,
},
},
{
Domain: "example.com",
Options: &godo.DomainRecordEditRequest{
Name: "@",
Type: endpoint.RecordTypeCNAME,
Data: "foo.example.com.",
TTL: digitalOceanRecordTTL,
},
},
}
if !elementsMatch(t, expectedCreates, changes.Creates) {
assert.Failf(t, "diff: %s", cmp.Diff(expectedCreates, changes.Creates))
}
}
func TestDigitalOceanProcessUpdateActions(t *testing.T) {
recordsByDomain := map[string][]godo.DomainRecord{
"example.com": {
{
ID: 1,
Name: "foo",
Type: endpoint.RecordTypeA,
Data: "1.2.3.4",
TTL: digitalOceanRecordTTL,
},
{
ID: 2,
Name: "foo",
Type: endpoint.RecordTypeA,
Data: "5.6.7.8",
TTL: digitalOceanRecordTTL,
},
{
ID: 3,
Name: "@",
Type: endpoint.RecordTypeCNAME,
Data: "foo.example.com.",
TTL: digitalOceanRecordTTL,
},
},
}
updatesByDomain := map[string][]*endpoint.Endpoint{
"example.com": {
endpoint.NewEndpoint("foo.example.com", endpoint.RecordTypeA, "10.11.12.13"),
endpoint.NewEndpoint("example.com", endpoint.RecordTypeCNAME, "bar.example.com"),
},
}
var changes digitalOceanChanges
err := processUpdateActions(recordsByDomain, updatesByDomain, &changes)
require.NoError(t, err)
assert.Equal(t, 2, len(changes.Creates))
assert.Equal(t, 0, len(changes.Updates))
assert.Equal(t, 3, len(changes.Deletes))
expectedCreates := []*digitalOceanChangeCreate{
{
Domain: "example.com",
Options: &godo.DomainRecordEditRequest{
Name: "foo",
Type: endpoint.RecordTypeA,
Data: "10.11.12.13",
TTL: digitalOceanRecordTTL,
},
},
{
Domain: "example.com",
Options: &godo.DomainRecordEditRequest{
Name: "@",
Type: endpoint.RecordTypeCNAME,
Data: "bar.example.com.",
TTL: digitalOceanRecordTTL,
},
},
}
if !elementsMatch(t, expectedCreates, changes.Creates) {
assert.Failf(t, "diff: %s", cmp.Diff(expectedCreates, changes.Creates))
}
expectedDeletes := []*digitalOceanChangeDelete{
{
Domain: "example.com",
RecordID: 1,
},
{
Domain: "example.com",
RecordID: 2,
},
{
Domain: "example.com",
RecordID: 3,
},
}
if !elementsMatch(t, expectedDeletes, changes.Deletes) {
assert.Failf(t, "diff: %s", cmp.Diff(expectedDeletes, changes.Deletes))
}
}
func TestDigitalOceanProcessDeleteActions(t *testing.T) {
recordsByDomain := map[string][]godo.DomainRecord{
"example.com": {
{
ID: 1,
Name: "foo",
Type: endpoint.RecordTypeA,
Data: "1.2.3.4",
TTL: digitalOceanRecordTTL,
},
// This record will not be deleted because it represents a target not specified to be deleted.
{
ID: 2,
Name: "foo",
Type: endpoint.RecordTypeA,
Data: "5.6.7.8",
TTL: digitalOceanRecordTTL,
},
{
ID: 3,
Name: "@",
Type: endpoint.RecordTypeCNAME,
Data: "foo.example.com.",
TTL: digitalOceanRecordTTL,
},
},
}
deletesByDomain := map[string][]*endpoint.Endpoint{
"example.com": {
endpoint.NewEndpoint("foo.example.com", endpoint.RecordTypeA, "1.2.3.4"),
endpoint.NewEndpoint("example.com", endpoint.RecordTypeCNAME, "foo.example.com"),
},
}
var changes digitalOceanChanges
err := processDeleteActions(recordsByDomain, deletesByDomain, &changes)
require.NoError(t, err)
assert.Equal(t, 0, len(changes.Creates))
assert.Equal(t, 0, len(changes.Updates))
assert.Equal(t, 2, len(changes.Deletes))
expectedDeletes := []*digitalOceanChangeDelete{
{
Domain: "example.com",
RecordID: 1,
},
{
Domain: "example.com",
RecordID: 3,
},
}
if !elementsMatch(t, expectedDeletes, changes.Deletes) {
assert.Failf(t, "diff: %s", cmp.Diff(expectedDeletes, changes.Deletes))
}
}
func TestNewDigitalOceanProvider(t *testing.T) {
_ = os.Setenv("DO_TOKEN", "xxxxxxxxxxxxxxxxx")
_, err := NewDigitalOceanProvider(context.Background(), endpoint.NewDomainFilter([]string{"ext-dns-test.zalando.to."}), true, 50)
@ -198,29 +526,62 @@ func TestNewDigitalOceanProvider(t *testing.T) {
}
}
func TestDigitalOceanGetRecordID(t *testing.T) {
p := &DigitalOceanProvider{}
func TestDigitalOceanGetMatchingDomainRecords(t *testing.T) {
records := []godo.DomainRecord{
{
ID: 1,
Name: "foo.com",
Name: "foo",
Type: endpoint.RecordTypeCNAME,
Data: "baz.org.",
},
{
ID: 2,
Name: "baz.de",
Name: "baz",
Type: endpoint.RecordTypeA,
Data: "1.2.3.4",
},
{
ID: 3,
Name: "baz",
Type: endpoint.RecordTypeA,
Data: "5.6.7.8",
},
{
ID: 4,
Name: "@",
Type: endpoint.RecordTypeA,
Data: "9.10.11.12",
},
}
assert.Equal(t, 1, p.getRecordID(records, godo.DomainRecord{
Name: "foo.com",
Type: endpoint.RecordTypeCNAME,
}))
assert.Equal(t, 0, p.getRecordID(records, godo.DomainRecord{
Name: "foo.com",
Type: endpoint.RecordTypeA,
}))
ep1 := endpoint.NewEndpoint("foo.com", endpoint.RecordTypeCNAME)
assert.Equal(t, 1, len(getMatchingDomainRecords(records, "com", ep1)))
ep2 := endpoint.NewEndpoint("foo.com", endpoint.RecordTypeA)
assert.Equal(t, 0, len(getMatchingDomainRecords(records, "com", ep2)))
ep3 := endpoint.NewEndpoint("baz.org", endpoint.RecordTypeA)
r := getMatchingDomainRecords(records, "org", ep3)
assert.Equal(t, 2, len(r))
assert.ElementsMatch(t, r, []godo.DomainRecord{
{
ID: 2,
Name: "baz",
Type: endpoint.RecordTypeA,
Data: "1.2.3.4",
},
{
ID: 3,
Name: "baz",
Type: endpoint.RecordTypeA,
Data: "5.6.7.8",
},
})
ep4 := endpoint.NewEndpoint("example.com", endpoint.RecordTypeA)
r2 := getMatchingDomainRecords(records, "example.com", ep4)
assert.Equal(t, 1, len(r2))
assert.Equal(t, "9.10.11.12", r2[0].Data)
}
func validateDigitalOceanZones(t *testing.T, zones []godo.Domain, expected []godo.Domain) {
@ -265,3 +626,36 @@ func TestDigitalOceanAllRecords(t *testing.T) {
t.Errorf("expected to fail, %s", err)
}
}
func TestDigitalOceanMergeRecordsByNameType(t *testing.T) {
xs := []*endpoint.Endpoint{
endpoint.NewEndpoint("foo.example.com", "A", "1.2.3.4"),
endpoint.NewEndpoint("bar.example.com", "A", "1.2.3.4"),
endpoint.NewEndpoint("foo.example.com", "A", "5.6.7.8"),
endpoint.NewEndpoint("foo.example.com", "CNAME", "somewhere.out.there.com"),
}
merged := mergeEndpointsByNameType(xs)
assert.Equal(t, 3, len(merged))
sort.SliceStable(merged, func(i, j int) bool {
if merged[i].DNSName != merged[j].DNSName {
return merged[i].DNSName < merged[j].DNSName
}
return merged[i].RecordType < merged[j].RecordType
})
assert.Equal(t, "bar.example.com", merged[0].DNSName)
assert.Equal(t, "A", merged[0].RecordType)
assert.Equal(t, 1, len(merged[0].Targets))
assert.Equal(t, "1.2.3.4", merged[0].Targets[0])
assert.Equal(t, "foo.example.com", merged[1].DNSName)
assert.Equal(t, "A", merged[1].RecordType)
assert.Equal(t, 2, len(merged[1].Targets))
assert.ElementsMatch(t, []string{"1.2.3.4", "5.6.7.8"}, merged[1].Targets)
assert.Equal(t, "foo.example.com", merged[2].DNSName)
assert.Equal(t, "CNAME", merged[2].RecordType)
assert.Equal(t, 1, len(merged[2].Targets))
assert.Equal(t, "somewhere.out.there.com", merged[2].Targets[0])
}

View File

@ -157,16 +157,18 @@ func (p *VultrProvider) submitChanges(ctx context.Context, changes []*VultrChang
for zoneName, changes := range zoneChanges {
for _, change := range changes {
log.WithFields(log.Fields{
"record": change.ResourceRecordSet.Name,
"type": change.ResourceRecordSet.Type,
"ttl": change.ResourceRecordSet.TTL,
"action": change.Action,
"zone": zoneName,
"record": change.ResourceRecordSet.Name,
"type": change.ResourceRecordSet.Type,
"ttl": change.ResourceRecordSet.TTL,
"priority": change.ResourceRecordSet.Priority,
"action": change.Action,
"zone": zoneName,
}).Info("Changing record.")
switch change.Action {
case vultrCreate:
err = p.client.DNSRecord.Create(ctx, zoneName, change.ResourceRecordSet.Type, change.ResourceRecordSet.Name, change.ResourceRecordSet.Data, change.ResourceRecordSet.TTL, change.ResourceRecordSet.Priority)
priority := getPriority(change.ResourceRecordSet.Priority)
err = p.client.DNSRecord.Create(ctx, zoneName, change.ResourceRecordSet.Type, change.ResourceRecordSet.Name, change.ResourceRecordSet.Data, change.ResourceRecordSet.TTL, priority)
if err != nil {
return err
}
@ -276,3 +278,11 @@ func (p *VultrProvider) getRecordID(ctx context.Context, zone string, record gov
return 0, fmt.Errorf("no record was found")
}
func getPriority(priority *int) int {
p := 0
if priority != nil {
p = *priority
}
return p
}

View File

@ -55,7 +55,6 @@ type Config struct {
KubeConfig string
KubeMaster string
ServiceTypeFilter []string
IstioIngressGatewayServices []string
CFAPIEndpoint string
CFUsername string
CFPassword string

View File

@ -155,6 +155,5 @@ func TestByNames(t *testing.T) {
}
var minimalConfig = &Config{
IstioIngressGatewayServices: []string{"istio-system/istio-ingressgateway"},
ContourLoadBalancerService: "heptio-contour/contour",
}