diff --git a/CHANGELOG.md b/CHANGELOG.md index d125dc359..9cddb9d4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +## v0.5.14 - 2019-05-14 + + - Docs: Update aws.md (#1009) @pawelprazak + - New provider TransIP (#1007) @skoef + - Docs: Add docker image faq (#1006) @Raffo + - DNSimple: Support apex records (#1004) @jbowes + - NS1: Add --ns1-endpoint and --ns1-ignoressl flags (#1002) @mburtless + - AWS: Cache the endpoints on the controller loop (#1001) @fraenkel + - Core: Supress Kubernetes logs (#991) @njuettner + - Core: distroless/static image (#989) @jharshman + - Core: Headless service missing DNS entry (#984) @yverbin + - New provider NS1 (#963) @mburtless + - Core: Add Cloud Foundry routes as a source (#955) @dgrizzanti + ## v0.5.13 - 2019-04-18 - Azure: Support multiple A targets (#987) @michaelfig diff --git a/Dockerfile b/Dockerfile index d11f684cd..457b9e78d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ # limitations under the License. # builder image -FROM golang as builder +FROM golang:1.12.5 as builder WORKDIR /github.com/kubernetes-incubator/external-dns COPY . . @@ -22,9 +22,10 @@ RUN make test RUN make build # final image -FROM registry.opensource.zalan.do/stups/alpine:latest +FROM alpine:3.9 LABEL maintainer="Team Teapot @ Zalando SE " +RUN apk add ca-certificates && update-ca-certificates COPY --from=builder /github.com/kubernetes-incubator/external-dns/build/external-dns /bin/external-dns USER nobody diff --git a/Dockerfile.mini b/Dockerfile.mini new file mode 100644 index 000000000..f3eef891c --- /dev/null +++ b/Dockerfile.mini @@ -0,0 +1,8 @@ +FROM golang:1.12.5 as builder +WORKDIR /external-dns +COPY . . +RUN make build + +FROM gcr.io/distroless/static +COPY --from=builder /external-dns/build/external-dns /external-dns +ENTRYPOINT ["./external-dns"] diff --git a/Makefile b/Makefile index 8ba62f20f..765fecb9f 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ test: go test -v -race $(shell go list ./... | grep -v /vendor/) # The build targets allow to build the binary and docker image -.PHONY: build build.docker +.PHONY: build build.docker build.mini BINARY ?= external-dns SOURCES = $(shell find . -name '*.go') @@ -61,5 +61,8 @@ build.push: build.docker build.docker: docker build --rm --tag "$(IMAGE):$(VERSION)" . +build.mini: + docker build --rm --tag "$(IMAGE):$(VERSION)" -f Dockerfile.mini . + clean: @rm -rf build diff --git a/README.md b/README.md index 85471e96c..83706d3f3 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,9 @@ ExternalDNS' current release is `v0.5`. This version allows you to keep selected * [Oracle Cloud Infrastructure DNS](https://docs.cloud.oracle.com/iaas/Content/DNS/Concepts/dnszonemanagement.htm) * [Linode DNS](https://www.linode.com/docs/networking/dns/) * [RFC2136](https://tools.ietf.org/html/rfc2136) +* [NS1](https://ns1.com/) +* [TransIP](https://www.transip.eu/domain-name/) + From this release, ExternalDNS can become aware of the records it is managing (enabled via `--registry=txt`), therefore ExternalDNS can safely manage non-empty hosted zones. We strongly encourage you to use `v0.5` (or greater) with `--registry=txt` enabled and `--txt-owner-id` set 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. @@ -78,6 +81,8 @@ The following table clarifies the current status of the providers according to t | Oracle Cloud Infrastructure DNS | Alpha | | Linode DNS | Alpha | | RFC2136 | Alpha | +| NS1 | Alpha | +| TransIP | Alpha | ## Running ExternalDNS: @@ -108,6 +113,8 @@ The following tutorials are provided: * [Oracle Cloud Infrastructure (OCI) DNS](docs/tutorials/oracle.md) * [Linode](docs/tutorials/linode.md) * [RFC2136](docs/tutorials/rfc2136.md) +* [NS1](docs/tutorials/ns1.md) +* [TransIP](docs/tutorials/transip.md) ### Running Locally @@ -230,6 +237,8 @@ Here's a rough outline on what is to come (subject to change): - [x] Support for PowerDNS - [x] Support for Linode - [x] Support for RcodeZero +- [x] Support for NS1 +- [x] Support for TransIP ### v0.6 diff --git a/controller/controller.go b/controller/controller.go index 769c6553a..dad7f3082 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -17,12 +17,14 @@ limitations under the License. package controller import ( + "context" "time" "github.com/prometheus/client_golang/prometheus" log "github.com/sirupsen/logrus" "github.com/kubernetes-incubator/external-dns/plan" + "github.com/kubernetes-incubator/external-dns/provider" "github.com/kubernetes-incubator/external-dns/registry" "github.com/kubernetes-incubator/external-dns/source" ) @@ -89,6 +91,8 @@ func (c *Controller) RunOnce() error { } registryEndpointsTotal.Set(float64(len(records))) + ctx := context.WithValue(context.Background(), provider.RecordsContextKey, records) + endpoints, err := c.Source.Endpoints() if err != nil { sourceErrors.Inc() @@ -104,7 +108,7 @@ func (c *Controller) RunOnce() error { plan = plan.Calculate() - err = c.Registry.ApplyChanges(plan.Changes) + err = c.Registry.ApplyChanges(ctx, plan.Changes) if err != nil { registryErrors.Inc() return err diff --git a/controller/controller_test.go b/controller/controller_test.go index 909c33a78..1cf68bdfd 100644 --- a/controller/controller_test.go +++ b/controller/controller_test.go @@ -17,7 +17,9 @@ limitations under the License. package controller import ( + "context" "errors" + "reflect" "testing" "github.com/kubernetes-incubator/external-dns/endpoint" @@ -42,7 +44,7 @@ func (p *mockProvider) Records() ([]*endpoint.Endpoint, error) { } // ApplyChanges validates that the passed in changes satisfy the assumtions. -func (p *mockProvider) ApplyChanges(changes *plan.Changes) error { +func (p *mockProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { if len(changes.Create) != len(p.ExpectChanges.Create) { return errors.New("number of created records is wrong") } @@ -71,6 +73,9 @@ func (p *mockProvider) ApplyChanges(changes *plan.Changes) error { } } + if !reflect.DeepEqual(ctx.Value(provider.RecordsContextKey), p.RecordsStore) { + return errors.New("context is wrong") + } return nil } diff --git a/docs/contributing/sources-and-providers.md b/docs/contributing/sources-and-providers.md index 3d51b3598..37c9e1163 100644 --- a/docs/contributing/sources-and-providers.md +++ b/docs/contributing/sources-and-providers.md @@ -25,6 +25,7 @@ All sources live in package `source`. * `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](../crd-source.md) documentation. +* `EmptySource`: returns an empty list of Endpoint objects for the purpose of testing and cleaning out entries. ### Providers diff --git a/docs/faq.md b/docs/faq.md index 8f35e0395..c4821b279 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -45,6 +45,7 @@ Currently, the following providers are supported: - Oracle Cloud Infrastructure DNS - Linode DNS - RFC2136 +- TransIP 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. @@ -251,3 +252,18 @@ Yes, give it the correct cross-account/assume-role permissions and use the `--aw ### 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 Docker image on Zalando's public Docker registry with the following name: + +``` +registry.opensource.zalan.do/teapot/external-dns +``` + +As tags, you can use your version of choice or use `latest` that always resolves to the latest tag. + +If you wish to build your own image, you can use the provided [Dockerfile](../Dockerfile) as a starting point. + +We are currently working with the Kubernetes community to provide official images for the project similarly to what is done with the other official Kubernetes projects, but we don't have an ETA on when those images will be available. \ No newline at end of file diff --git a/docs/ttl.md b/docs/ttl.md index c290ea266..35295e90b 100644 --- a/docs/ttl.md +++ b/docs/ttl.md @@ -27,6 +27,7 @@ Providers - [x] Google - [ ] InMemory - [x] Linode +- [x] TransIP PRs welcome! @@ -51,4 +52,7 @@ For the moment, it is impossible to use a TTL value of 0 with the AWS, DigitalOc 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 \ No newline at end of file +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. diff --git a/docs/tutorials/aws.md b/docs/tutorials/aws.md index f10d3d379..88a7a75a2 100644 --- a/docs/tutorials/aws.md +++ b/docs/tutorials/aws.md @@ -119,7 +119,7 @@ rules: verbs: ["get","watch","list"] - apiGroups: [""] resources: ["nodes"] - verbs: ["list"] + verbs: ["list","watch"] --- apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRoleBinding diff --git a/docs/tutorials/cloudflare.md b/docs/tutorials/cloudflare.md index e4c191690..d1bce19e2 100644 --- a/docs/tutorials/cloudflare.md +++ b/docs/tutorials/cloudflare.md @@ -165,6 +165,7 @@ 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. diff --git a/docs/tutorials/ns1.md b/docs/tutorials/ns1.md new file mode 100644 index 000000000..607cf8221 --- /dev/null +++ b/docs/tutorials/ns1.md @@ -0,0 +1,200 @@ +# 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](https://ns1.com/knowledgebase/creating-a-zone) + +[Creating a zone using the NS1 +API](https://ns1.com/api#put-create-a-new-dns-zone) + +## 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](http://my.nsone.net). + +2. Click your username in the upper-right corner, and navigate to **Account Settings** \> **Users & Teams**. + +3. Navigate to the _API Keys_ tab, and click **Add Key**. + +4. 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](https://ns1.com/knowledgebase/creating-and-managing-users) in the NS1 Knowledge Base. + +## 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) + +```yaml +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: external-dns +spec: + strategy: + type: Recreate + template: + metadata: + labels: + app: external-dns + spec: + containers: + - name: external-dns + image: registry.opensource.zalan.do/teapot/external-dns:latest + 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) + +```yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: external-dns +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: ClusterRole +metadata: + name: external-dns +rules: +- apiGroups: [""] + resources: ["services"] + verbs: ["get","watch","list"] +- apiGroups: [""] + resources: ["pods"] + verbs: ["get","watch","list"] +- apiGroups: ["extensions"] + resources: ["ingresses"] + verbs: ["get","watch","list"] +- apiGroups: [""] + resources: ["nodes"] + verbs: ["list"] +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +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: extensions/v1beta1 +kind: Deployment +metadata: + name: external-dns +spec: + strategy: + type: Recreate + template: + metadata: + labels: + app: external-dns + spec: + serviceAccountName: external-dns + containers: + - name: external-dns + image: registry.opensource.zalan.do/teapot/external-dns:latest + args: + - --source=service # 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: + +```yaml +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: nginx +spec: + 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 +``` diff --git a/docs/tutorials/transip.md b/docs/tutorials/transip.md new file mode 100644 index 000000000..8287baebb --- /dev/null +++ b/docs/tutorials/transip.md @@ -0,0 +1,181 @@ +# 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](https://www.transip.eu/knowledgebase/entry/77-want-use-the-transip-api/). With the private key generated by the API, we create a kubernetes secret: + +```console +$ 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) + +```yaml +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: external-dns +spec: + strategy: + type: Recreate + template: + metadata: + labels: + app: external-dns + spec: + containers: + - name: external-dns + image: registry.opensource.zalan.do/teapot/external-dns:latest + 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) + +```yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: external-dns +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: ClusterRole +metadata: + name: external-dns +rules: +- apiGroups: [""] + resources: ["services"] + verbs: ["get","watch","list"] +- apiGroups: [""] + resources: ["pods"] + verbs: ["get","watch","list"] +- apiGroups: ["extensions"] + resources: ["ingresses"] + verbs: ["get","watch","list"] +- apiGroups: [""] + resources: ["nodes"] + verbs: ["watch", "list"] +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +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: extensions/v1beta1 +kind: Deployment +metadata: + name: external-dns +spec: + strategy: + type: Recreate + template: + metadata: + labels: + app: external-dns + spec: + serviceAccountName: external-dns + containers: + - name: external-dns + image: registry.opensource.zalan.do/teapot/external-dns:latest + args: + - --source=service # 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: + +```yaml +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: nginx +spec: + 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: + +```console +$ 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](https://transip.eu/cp) 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. diff --git a/go.mod b/go.mod index 3ad9578aa..2e270938d 100644 --- a/go.mod +++ b/go.mod @@ -11,9 +11,10 @@ require ( github.com/alecthomas/kingpin v2.2.5+incompatible github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1 // indirect github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20180828111155-cad214d7d71f - github.com/aws/aws-sdk-go v1.13.32 + github.com/aws/aws-sdk-go v1.19.41 github.com/cenkalti/backoff v2.1.1+incompatible // indirect github.com/cloudflare/cloudflare-go v0.0.0-20190102215809-0c85496d8730 + github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381 github.com/coreos/bbolt v1.3.2 // indirect github.com/coreos/etcd v3.3.10+incompatible github.com/coreos/go-semver v0.2.0 // indirect @@ -27,11 +28,9 @@ require ( github.com/envoyproxy/go-control-plane v0.6.9 // indirect github.com/exoscale/egoscale v0.11.0 github.com/ffledgling/pdns-go v0.0.0-20180219074714-524e7daccd99 - github.com/go-ini/ini v1.32.0 // indirect github.com/go-resty/resty v1.8.0 // indirect github.com/gogo/googleapis v1.1.0 // indirect github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef // indirect - github.com/golang/mock v1.2.0 // indirect github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a // indirect github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135 // indirect github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf // indirect @@ -46,7 +45,6 @@ require ( github.com/imdario/mergo v0.3.5 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/infobloxopen/infoblox-go-client v0.0.0-20180606155407-61dc5f9b0a65 - github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8 // indirect github.com/jonboulle/clockwork v0.1.0 // indirect github.com/json-iterator/go v1.1.6 // indirect github.com/linki/instrumented_http v0.2.0 @@ -64,7 +62,7 @@ require ( github.com/onsi/gomega v1.5.0 // indirect github.com/oracle/oci-go-sdk v1.8.0 github.com/peterbourgon/diskv v2.0.1+incompatible // indirect - github.com/pkg/errors v0.8.0 + github.com/pkg/errors v0.8.1 github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829 github.com/sanyu/dynectsoap v0.0.0-20181203081243-b83de5edc4e0 github.com/satori/go.uuid v1.2.0 // indirect @@ -77,6 +75,7 @@ require ( github.com/stretchr/testify v1.2.2 github.com/tent/http-link-go v0.0.0-20130702225549-ac974c61c2f9 // indirect github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8 // indirect + github.com/transip/gotransip v5.8.2+incompatible github.com/ugorji/go/codec v0.0.0-20190320090025-2dc34c0b8780 // indirect github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect github.com/yl2chen/cidranger v0.0.0-20180214081945-928b519e5268 // indirect @@ -89,8 +88,8 @@ require ( google.golang.org/api v0.3.0 google.golang.org/appengine v1.5.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/ini.v1 v1.42.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect + gopkg.in/ns1/ns1-go.v2 v2.0.0-20190322154155-0dafb5275fd1 gopkg.in/yaml.v2 v2.2.2 istio.io/api v0.0.0-20190321180614-db16d82d3672 istio.io/istio v0.0.0-20190322063008-2b1331886076 @@ -101,3 +100,5 @@ require ( k8s.io/kube-openapi v0.0.0-20190401085232-94e1e7b7574c // indirect launchpad.net/gocheck v0.0.0-20140225173054-000000000087 // indirect ) + +replace github.com/golang/glog => github.com/kubermatic/glog-logrus v0.0.0-20180829085450-3fa5b9870d1d diff --git a/go.sum b/go.sum index 56c515220..66b359161 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,8 @@ -cloud.google.com/go v0.26.0 h1:e0WKqKTd5BnrG8aKH3J3h+QvEIQtSUcf2n5UZ5ZgLtQ= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +code.cloudfoundry.org/gofileutils v0.0.0-20170111115228-4d0c80011a0f h1:UrKzEwTgeiff9vxdrfdqxibzpWjxLnuXDI5m6z3GJAk= +code.cloudfoundry.org/gofileutils v0.0.0-20170111115228-4d0c80011a0f/go.mod h1:sk5LnIjB/nIEU7yP5sDQExVm62wu0pBh3yrElngUisI= git.apache.org/thrift.git v0.12.0/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= github.com/Azure/azure-sdk-for-go v10.0.4-beta+incompatible h1:FhnlL7/4O3gAB7EBgN43vA3Bb0fAlCBIMm9avXbcHlE= github.com/Azure/azure-sdk-for-go v10.0.4-beta+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= @@ -9,9 +10,9 @@ github.com/Azure/go-autorest v10.9.0+incompatible h1:3ccqKLQg+scl0J6krcDgih2Rl+G github.com/Azure/go-autorest v10.9.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/Shopify/sarama v1.19.0 h1:9oksLxC6uxVPHPVYUmq6xhr1BOF/hHobWH2UzO67z1s= +github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc= +github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= -github.com/Shopify/toxiproxy v2.1.4+incompatible h1:TKdv8HiTLgE5wdJuEML90aBgNWsokNbMijUGhmcoBJc= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U= github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= @@ -27,20 +28,21 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZq github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20180828111155-cad214d7d71f h1:hinXH9rcBjRoIih5tl4f1BCbNjOmPJ2UnZwcYDhEHR0= github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20180828111155-cad214d7d71f/go.mod h1:T9M45xf79ahXVelWoOBmH0y4aC1t5kXO5BxwyakgIGA= -github.com/apache/thrift v0.12.0 h1:pODnxUFNcjP9UTLZGTdeh+j16A8lJbRvD3rOtrk/7bs= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= -github.com/aws/aws-sdk-go v1.13.32 h1:AoV2boU+diwKoMaschMtUJim3nmBpM/4y45UqY708F4= -github.com/aws/aws-sdk-go v1.13.32/go.mod h1:ZRmQr0FajVIyZ4ZzBYKG5P3ZqPz9IHG41ZoMu1ADI3k= +github.com/aws/aws-sdk-go v1.19.41 h1:veutzvQP/lOmYmtX26S9mTFJLO6sp7/UsxFcCjglu4A= +github.com/aws/aws-sdk-go v1.19.41/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/cenkalti/backoff v2.1.1+incompatible h1:tKJnvO2kl0zmb/jA5UKAt4VoEVw1qxKWjE/Bpp46npY= github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= -github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/cloudflare-go v0.0.0-20190102215809-0c85496d8730 h1:+TK6ytATp7coqI4UlTBboFYD0kSkWZt6L6/T+1yBK6k= github.com/cloudflare/cloudflare-go v0.0.0-20190102215809-0c85496d8730/go.mod h1:qKQ9S///VKEax9N8kFel9/AvmnkYgvb8uiKTnoVFvpg= -github.com/codegangsta/cli v1.20.0 h1:iX1FXEgwzd5+XN6wk5cVHOGQj6Q3Dcp20lUeS4lHNTw= +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/codegangsta/cli v1.20.0/go.mod h1:/qJNoX69yVSKu5o4jLyXAENLRyk1uhi7zkbQ3slBdOA= +github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 h1:sDMmm+q/3+BukdIpxwO365v/Rbspp2Nt5XntgQRXq8Q= +github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= github.com/coreos/bbolt v1.3.2 h1:wZwiHHUieZCquLkDL0B8UhzreNWsPHooDAG3q34zk0s= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible h1:jFneRYjIvLMLhDLCzuTuU4rSJUjRplcJQ7pD7MnhC04= @@ -63,11 +65,8 @@ github.com/dnaeon/go-vcr v1.0.1 h1:r8L/HqC0Hje5AXMu1ooW8oyQyOFv4GxqpL0nRP7SLLY= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= github.com/dnsimple/dnsimple-go v0.14.0 h1:JGYtVVA/uHc91q0LjDWqR1oVj6EGu9Kn0lMRxjH/w30= github.com/dnsimple/dnsimple-go v0.14.0/go.mod h1:0FYu4qVNv/UcfZPNwa9zi68IkggJu3TIwM54D7rhmI4= -github.com/eapache/go-resiliency v1.1.0 h1:1NtRmCAqadE2FN4ZcN6g90TP3uk8cg9rn9eNK2197aU= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= -github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 h1:YEetp8/yCZMuEPMUDHG0CW/brkkEp8mzqk2+ODEitlw= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= -github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/envoyproxy/go-control-plane v0.6.9 h1:deEH9W8ZAUGNbCdX+9iNzBOGrAOrnpJGoy0PcTqk/tE= github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= @@ -79,34 +78,25 @@ github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-ini/ini v1.32.0 h1:/MArBHSS0TFR28yPPDK1vPIjt4wUnPBfb81i6iiyKvA= -github.com/go-ini/ini v1.32.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= -github.com/go-kit/kit v0.8.0 h1:Wz+5lgoB0kkuqLEc6NVmwRknTKP6dTGbSqvhZtBI/j0= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-logfmt/logfmt v0.3.0 h1:8HUsc87TaSWLKwrnumgC8/YconD2fJQsRJAsWaPg2ic= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab h1:xveKWz2iaueeTaUgdetzel+U7exyigDYBryyVfV/rZk= +github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= github.com/go-resty/resty v1.8.0 h1:vbNCxbHOWCototzwxf3L63PQCKx6xgT6v8SHfoqkp6U= github.com/go-resty/resty v1.8.0/go.mod h1:n37daLLGIHq2FFYHxg+FYQiwA95FpfNI+A9uxoIYGRk= -github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gogo/googleapis v1.1.0 h1:kFkMAZBNAn4j7K0GiZr8cRYzejq68VbheufiV3YuyFI= github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= -github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0 h1:xU6/SpYbvkNYiptHJYEDRseDLvYE7wSqhYYNy0QSUzI= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef h1:veQD95Isof8w9/WXiA+pa3tz3fJXkt5B7QaRBrM62gk= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= -github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a h1:ZJu5NB1Bk5ms4vw0Xu4i+jD32SE9jQXyfnOvwhHqlT0= github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -120,6 +110,7 @@ github.com/googleapis/gnostic v0.2.0 h1:l6N3VoaVzTncYYW+9yOz2LJJammFZGBO13sqgEhp github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/gophercloud/gophercloud v0.0.0-20190126172459-c818fa66e4c8 h1:L9JPKrtsHMQ4VCRQfHvbbHBfB2Urn8xf6QZeXZ+OrN4= github.com/gophercloud/gophercloud v0.0.0-20190126172459-c818fa66e4c8/go.mod h1:3WdhXV3rUYy9p6AUW8d94kr+HS62Y4VL9mBnFxsD8q4= +github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= @@ -151,37 +142,37 @@ github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NH github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/infobloxopen/infoblox-go-client v0.0.0-20180606155407-61dc5f9b0a65 h1:FP5rOFP4ifbtFIjFHJmwhFrsbDyONILK/FNntl/Pou8= github.com/infobloxopen/infoblox-go-client v0.0.0-20180606155407-61dc5f9b0a65/go.mod h1:BXiw7S2b9qJoM8MS40vfgCNB2NLHGusk1DtO16BD9zI= -github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8 h1:12VvqtR6Aowv3l/EQUlocDHW2Cp4G9WJVH7uyH8QFJE= -github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/julienschmidt/httprouter v1.2.0 h1:TDTW5Yz1mjftljbcKqRcrYhd4XeOoI98t+9HbQbYf7g= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kubermatic/glog-logrus v0.0.0-20180829085450-3fa5b9870d1d h1:JV46OtdhH2vVt8mJ1EWUE94k99vbN9fZs1WQ8kcEapU= +github.com/kubermatic/glog-logrus v0.0.0-20180829085450-3fa5b9870d1d/go.mod h1:CHQ3o5KBH1PIS2Fb1mRLTIWO5YzP9kSUB3KoCICwlvA= github.com/linki/instrumented_http v0.2.0 h1:zLhcB3Q/McQQqml3qd5kzdZ0cGnL3vquPFIW2338f5Y= github.com/linki/instrumented_http v0.2.0/go.mod h1:pjYbItoegfuVi2GUOMhEqzvm/SJKuEL3H0tc8QRLRFk= github.com/linode/linodego v0.3.0 h1:I83pEPg4owSy5pCPaKix7xkGbWIjPxmAoc/Yu5OYDDY= github.com/linode/linodego v0.3.0/go.mod h1:ga11n3ivecUrPCHN0rANxKmfWBJVkOXfLMZinAbj2sY= github.com/lyft/protoc-gen-validate v0.0.14 h1:xbdDVIHd0Xq5Bfzu+8JR9s7mFmJPMvNLmfGhgcHJdFU= github.com/lyft/protoc-gen-validate v0.0.14/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= +github.com/martini-contrib/render v0.0.0-20150707142108-ec18f8345a11 h1:YFh+sjyJTMQSYjKwM4dFKhJPJC/wfo98tPUc17HdoYw= +github.com/martini-contrib/render v0.0.0-20150707142108-ec18f8345a11/go.mod h1:Ah2dBMoxZEqk118as2T4u4fjfXarE0pPnMJaArZQZsI= github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4= github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= @@ -193,7 +184,6 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223 h1:F9x/1yl3T2AeKLr2AMdilSD8+f9bvMnNN8VS5iDtovc= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM= github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk= @@ -201,7 +191,6 @@ github.com/nesv/go-dynect v0.6.0 h1:Ow/DiSm4LAISwnFku/FITSQHnU6pBvhQMsUE5Gu6Oq4= github.com/nesv/go-dynect v0.6.0/go.mod h1:GHRBRKzTwjAMhosHJQq/KrZaFkXIFyJ5zRE7thGXXrs= github.com/nic-at/rc0go v1.1.0 h1:k6/Bru/npTjmCSFw65ulYRw/b3ycIS30t6/YM4r42V4= github.com/nic-at/rc0go v1.1.0/go.mod h1:KEa3H5fmDNXCaXSqOeAZxkKnG/8ggr1OHIG25Ve7fjU= -github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88= github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -211,22 +200,23 @@ github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1Cpa github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/openzipkin/zipkin-go v0.1.3/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= -github.com/openzipkin/zipkin-go v0.1.6 h1:yXiysv1CSK7Q5yjGy1710zZGnsbMUIjluWBxtLXHPBo= github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/oracle/oci-go-sdk v1.8.0 h1:4SO45bKV0I3/Mn1os3ANDZmV0eSE5z5CLdSUIkxtyzs= github.com/oracle/oci-go-sdk v1.8.0/go.mod h1:VQb79nF8Z2cwLkLS35ukwStZIg5F66tcBccjip/j888= +github.com/oxtoacart/bpool v0.0.0-20150712133111-4e1c5567d7c2 h1:CXwSGu/LYmbjEab5aMCs5usQRVBGThelUKBNnoSOuso= +github.com/oxtoacart/bpool v0.0.0-20150712133111-4e1c5567d7c2/go.mod h1:L3UMQOThbttwfYRNFOWLLVXMhk5Lkio4GGOtw5UrxS0= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= -github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829 h1:D+CiwcpGTW6pL6bv6KI3KbyEyCKyS+1JWS2h8PNDnGA= github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f h1:BVwpUVJDADN2ufcGik7W992pyps0wZ888b/y9GXcLTU= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= @@ -235,9 +225,7 @@ github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1 h1:/K3IL0Z1quvmJ7X0A1AwNEK7CRkVK3YwfOU/QAL4WGg= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a h1:9ZKAASQSHhDYGoxY8uLVpewe1GDZ2vu2Tr/vTdVAkFQ= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af h1:gu+uRPtBe88sKxUCEXRoeCvVG90TJmwhiqRpvdhQFng= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/sanyu/dynectsoap v0.0.0-20181203081243-b83de5edc4e0 h1:vOcHdR1nu7DO4BAx1rwzdHV7jQTzW3gqcBT5qxHSc6A= github.com/sanyu/dynectsoap v0.0.0-20181203081243-b83de5edc4e0/go.mod h1:FeplEtXXejBYC4NPAFTrs5L7KuK+5RL9bf5nB2vZe9o= @@ -247,8 +235,10 @@ github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/smartystreets/assertions v0.0.0-20180725160413-e900ae048470/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a h1:pa8hGb/2YqsZKovtsgrwcDH1RZhVbTKCjLp47XpqCDs= github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.3 h1:09wy7WZk4AqO03yH85Ex1X+Uo3vDsil3Fa9AgF8Emss= @@ -265,6 +255,8 @@ github.com/tent/http-link-go v0.0.0-20130702225549-ac974c61c2f9 h1:/Bsw4C+DEdqPj github.com/tent/http-link-go v0.0.0-20130702225549-ac974c61c2f9/go.mod h1:RHkNRtSLfOK7qBTHaeSX1D6BNpI3qw7NTxsmNr4RvN8= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8 h1:ndzgwNDnKIqyCvHTXaCqh9KlOWKvBry6nuXMJmonVsE= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/transip/gotransip v5.8.2+incompatible h1:aNJhw/w/3QBqFcHAIPz1ytoK5FexeMzbUCGrrhWr3H0= +github.com/transip/gotransip v5.8.2+incompatible/go.mod h1:uacMoJVmrfOcscM4Bi5NVg708b7c6rz2oDTWqa7i2Ic= github.com/ugorji/go v1.1.2 h1:JON3E2/GPW2iDNGoSAusl1KDf5TRQ8k8q7Tp097pZGs= github.com/ugorji/go v1.1.2/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ= github.com/ugorji/go/codec v0.0.0-20190320090025-2dc34c0b8780 h1:vG/gY/PxA3v3l04qxe3tDjXyu3bozii8ulSlIPOYKhI= @@ -287,14 +279,11 @@ go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4 h1:c2HOrn5iMezYjSlGPncknSEr/8x5LELb/ilJbXi9DEA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3 h1:x/bBzNauLQAlE3fLku/xy92Y8QwKX5HZymrMz2IiKFc= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f h1:hX65Cu3JDlGH3uEdK7I99Ii+9kjD6mvnnpfLdEAH0x4= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/net v0.0.0-20180611182652-db08ff08e862/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -303,16 +292,15 @@ golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3 h1:eH6Eip3UpmR+yM/qI9Ijluzb1bNv/cAU/n+6l8tRSis= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190130055435-99b60b757ec1/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 h1:Wo7BWFiOk0QRFMLYMqJGFMd9CgUAcGx7V+qEg/h5IBI= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -328,7 +316,6 @@ golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181218192612-074acd46bca6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -341,13 +328,11 @@ golang.org/x/tools v0.0.0-20181219222714-6e267b5cc78e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384 h1:TFlARGu6Czu1z7q93HTxcP1P+/ZFC/IKythI5RzrnRg= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= google.golang.org/api v0.0.0-20181220000619-583d854617af/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.2.0/go.mod h1:IfRCZScioGtypHNTlz3gFk67J8uePVW7uDTBzXuIkhU= google.golang.org/api v0.3.0 h1:UIJY20OEo3+tK5MBlcdx37kmdH6EnRjGkW78mc6+EeA= google.golang.org/api v0.3.0/go.mod h1:IuvZyQh8jgscv8qWfQ4ABd8m7hEudgBFM/EdhA3BnXw= -google.golang.org/appengine v1.1.0 h1:igQkv0AAhEIvTEpD5LIpAfav2eeVO9HBTjvKHVJPRSs= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -361,9 +346,7 @@ google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9M google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0 h1:cfg4PD8YEdSFnm7qLV4++93WcmhH2nIUhMjhdCvl3j8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -371,22 +354,20 @@ gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/ini.v1 v1.42.0 h1:7N3gPTt50s8GuLortA00n8AqRTk75qOP98+mTPpgzRk= -gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/ns1/ns1-go.v2 v2.0.0-20190322154155-0dafb5275fd1 h1:+fgY/3ngqdBW9oLQCMwL5g+QRkKFPJH05fx2/pipqRQ= +gopkg.in/ns1/ns1-go.v2 v2.0.0-20190322154155-0dafb5275fd1/go.mod h1:VV+3haRsgDiVLxyifmMBrBIuCWFBPYKbRssXB9z67Hw= gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= -gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20180920025451-e3ad64cb4ed3/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099 h1:XJP7lxbSxWLOMNdBE4B/STaqVy6L73o0knwj2vIlxnw= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= istio.io/api v0.0.0-20190321180614-db16d82d3672 h1:luY97pBVarSo1v++zf2kgb84Q55G5hv/ult2A4KPQuk= istio.io/api v0.0.0-20190321180614-db16d82d3672/go.mod h1:hhLFQmpHia8zgaM37vb2ml9iS5NfNfqZGRt1pS9aVEo= diff --git a/main.go b/main.go index 5e2354cd4..ad78f0276 100644 --- a/main.go +++ b/main.go @@ -82,6 +82,9 @@ func main() { KubeMaster: cfg.Master, ServiceTypeFilter: cfg.ServiceTypeFilter, IstioIngressGatewayServices: cfg.IstioIngressGatewayServices, + CFAPIEndpoint: cfg.CFAPIEndpoint, + CFUsername: cfg.CFUsername, + CFPassword: cfg.CFPassword, } // Lookup all the selected sources by names and pass them the desired configuration. @@ -97,7 +100,7 @@ func main() { // Combine multiple sources into a single, deduplicated source. endpointsSource := source.NewDedupSource(source.NewMultiSource(sources)) - domainFilter := provider.NewDomainFilter(cfg.DomainFilter) + domainFilter := provider.NewDomainFilterWithExclusions(cfg.DomainFilter, cfg.ExcludeDomains) zoneIDFilter := provider.NewZoneIDFilter(cfg.ZoneIDFilter) zoneTypeFilter := provider.NewZoneTypeFilter(cfg.AWSZoneType) zoneTagFilter := provider.NewZoneTagFilter(cfg.AWSZoneTagFilter) @@ -154,6 +157,7 @@ func main() { Version: cfg.InfobloxWapiVersion, SSLVerify: cfg.InfobloxSSLVerify, View: cfg.InfobloxView, + MaxResults: cfg.InfobloxMaxResults, DryRun: cfg.DryRun, }, ) @@ -201,6 +205,18 @@ func main() { } case "rfc2136": p, err = provider.NewRfc2136Provider(cfg.RFC2136Host, cfg.RFC2136Port, cfg.RFC2136Zone, cfg.RFC2136Insecure, cfg.RFC2136TSIGKeyName, cfg.RFC2136TSIGSecret, cfg.RFC2136TSIGSecretAlg, cfg.RFC2136TAXFR, domainFilter, cfg.DryRun, nil) + case "ns1": + p, err = provider.NewNS1Provider( + provider.NS1Config{ + DomainFilter: domainFilter, + ZoneIDFilter: zoneIDFilter, + NS1Endpoint: cfg.NS1Endpoint, + NS1IgnoreSSL: cfg.NS1IgnoreSSL, + DryRun: cfg.DryRun, + }, + ) + case "transip": + p, err = provider.NewTransIPProvider(cfg.TransIPAccountName, cfg.TransIPPrivateKeyFile, domainFilter, cfg.DryRun) default: log.Fatalf("unknown dns provider: %s", cfg.Provider) } diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index ad497fb87..10586e66c 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -54,6 +54,7 @@ type Config struct { Provider string GoogleProject string DomainFilter []string + ExcludeDomains []string ZoneIDFilter []string AlibabaCloudConfigFile string AlibabaCloudZoneType string @@ -76,6 +77,7 @@ type Config struct { InfobloxWapiVersion string InfobloxSSLVerify bool InfobloxView string + InfobloxMaxResults int DynCustomerName string DynUsername string DynPassword string `secure:"yes"` @@ -105,6 +107,9 @@ type Config struct { CRDSourceAPIVersion string CRDSourceKind string ServiceTypeFilter []string + CFAPIEndpoint string + CFUsername string + CFPassword string RFC2136Host string RFC2136Port int RFC2136Zone string @@ -113,6 +118,10 @@ type Config struct { RFC2136TSIGSecret string `secure:"yes"` RFC2136TSIGSecretAlg string RFC2136TAXFR bool + NS1Endpoint string + NS1IgnoreSSL bool + TransIPAccountName string + TransIPPrivateKeyFile string } var defaultConfig = &Config{ @@ -133,6 +142,7 @@ var defaultConfig = &Config{ Provider: "", GoogleProject: "", DomainFilter: []string{}, + ExcludeDomains: []string{}, AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json", AWSZoneType: "", AWSZoneTagFilter: []string{}, @@ -153,6 +163,7 @@ var defaultConfig = &Config{ InfobloxWapiVersion: "2.3.1", InfobloxSSLVerify: true, InfobloxView: "", + InfobloxMaxResults: 0, OCIConfigFile: "/etc/kubernetes/oci.yaml", InMemoryZones: []string{}, PDNSServer: "http://localhost:8081", @@ -178,6 +189,9 @@ var defaultConfig = &Config{ CRDSourceAPIVersion: "externaldns.k8s.io/v1alpha1", CRDSourceKind: "DNSEndpoint", ServiceTypeFilter: []string{}, + CFAPIEndpoint: "", + CFUsername: "", + CFPassword: "", RFC2136Host: "", RFC2136Port: 0, RFC2136Zone: "", @@ -186,6 +200,10 @@ var defaultConfig = &Config{ RFC2136TSIGSecret: "", RFC2136TSIGSecretAlg: "", RFC2136TAXFR: true, + NS1Endpoint: "", + NS1IgnoreSSL: false, + TransIPAccountName: "", + TransIPPrivateKeyFile: "", } // NewConfig returns new Config object @@ -237,8 +255,13 @@ func (cfg *Config) ParseFlags(args []string) error { // Flags related to Istio app.Flag("istio-ingress-gateway", "The fully-qualified name of the Istio ingress gateway service. Flag can be specified multiple times (default: istio-system/istio-ingressgateway)").Default("istio-system/istio-ingressgateway").StringsVar(&cfg.IstioIngressGatewayServices) + // Flags related to cloud foundry + app.Flag("cf-api-endpoint", "The fully-qualified domain name of the cloud foundry instance you are targeting").Default(defaultConfig.CFAPIEndpoint).StringVar(&cfg.CFAPIEndpoint) + app.Flag("cf-username", "The username to log into the cloud foundry API").Default(defaultConfig.CFUsername).StringVar(&cfg.CFUsername) + app.Flag("cf-password", "The password to log into the cloud foundry API").Default(defaultConfig.CFPassword).StringVar(&cfg.CFPassword) + // Flags related to processing sources - app.Flag("source", "The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress, fake, connector, istio-gateway, crd").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress", "istio-gateway", "fake", "connector", "crd") + app.Flag("source", "The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress, fake, connector, istio-gateway, cloudfoundry, crd, empty").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress", "istio-gateway", "cloudfoundry", "fake", "connector", "crd", "empty") app.Flag("namespace", "Limit sources of endpoints to a specific namespace (default: all namespaces)").Default(defaultConfig.Namespace).StringVar(&cfg.Namespace) app.Flag("annotation-filter", "Filter sources managed by external-dns via annotation using label selector semantics (default: all sources)").Default(defaultConfig.AnnotationFilter).StringVar(&cfg.AnnotationFilter) app.Flag("fqdn-template", "A templated string that's used to generate DNS names from sources that don't define a hostname themselves, or to add a hostname suffix when paired with the fake source (optional). Accepts comma separated list for multiple global FQDN.").Default(defaultConfig.FQDNTemplate).StringVar(&cfg.FQDNTemplate) @@ -253,8 +276,9 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("service-type-filter", "The service types to take care about (default: all, expected: ClusterIP, NodePort, LoadBalancer or ExternalName)").StringsVar(&cfg.ServiceTypeFilter) // Flags related to providers - app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, aws-sd, google, azure, cloudflare, rcodezero, digitalocean, dnsimple, infoblox, dyn, designate, coredns, skydns, inmemory, pdns, oci, exoscale, linode, rfc2136)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "aws-sd", "google", "azure", "alibabacloud", "cloudflare", "rcodezero", "digitalocean", "dnsimple", "infoblox", "dyn", "designate", "coredns", "skydns", "inmemory", "pdns", "oci", "exoscale", "linode", "rfc2136") + app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, aws-sd, google, azure, cloudflare, rcodezero, digitalocean, dnsimple, infoblox, dyn, designate, coredns, skydns, inmemory, pdns, oci, exoscale, linode, rfc2136, ns1, transip)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "aws-sd", "google", "azure", "alibabacloud", "cloudflare", "rcodezero", "digitalocean", "dnsimple", "infoblox", "dyn", "designate", "coredns", "skydns", "inmemory", "pdns", "oci", "exoscale", "linode", "rfc2136", "ns1", "transip") app.Flag("domain-filter", "Limit possible target zones by a domain suffix; specify multiple times for multiple domains (optional)").Default("").StringsVar(&cfg.DomainFilter) + app.Flag("exclude-domains", "Exclude subdomains (optional)").Default("").StringsVar(&cfg.ExcludeDomains) app.Flag("zone-id-filter", "Filter target zones by hosted zone id; specify multiple times for multiple zones (optional)").Default("").StringsVar(&cfg.ZoneIDFilter) app.Flag("google-project", "When using the Google provider, current project is auto-detected, when running on GCP. Specify other project with this. Must be specified when running outside GCP.").Default(defaultConfig.GoogleProject).StringVar(&cfg.GoogleProject) app.Flag("alibaba-cloud-config-file", "When using the Alibaba Cloud provider, specify the Alibaba Cloud configuration file (required when --provider=alibabacloud").Default(defaultConfig.AlibabaCloudConfigFile).StringVar(&cfg.AlibabaCloudConfigFile) @@ -277,6 +301,7 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("infoblox-wapi-version", "When using the Infoblox provider, specify the WAPI version (default: 2.3.1)").Default(defaultConfig.InfobloxWapiVersion).StringVar(&cfg.InfobloxWapiVersion) app.Flag("infoblox-ssl-verify", "When using the Infoblox provider, specify whether to verify the SSL certificate (default: true, disable with --no-infoblox-ssl-verify)").Default(strconv.FormatBool(defaultConfig.InfobloxSSLVerify)).BoolVar(&cfg.InfobloxSSLVerify) app.Flag("infoblox-view", "DNS view (default: \"\")").Default(defaultConfig.InfobloxView).StringVar(&cfg.InfobloxView) + app.Flag("infoblox-max-results", "Add _max_results as query parameter to the URL on all API requests. The default is 0 which means _max_results is not set and the default of the server is used.").Default(strconv.Itoa(defaultConfig.InfobloxMaxResults)).IntVar(&cfg.InfobloxMaxResults) app.Flag("dyn-customer-name", "When using the Dyn provider, specify the Customer Name").Default("").StringVar(&cfg.DynCustomerName) app.Flag("dyn-username", "When using the Dyn provider, specify the Username").Default("").StringVar(&cfg.DynUsername) app.Flag("dyn-password", "When using the Dyn provider, specify the pasword").Default("").StringVar(&cfg.DynPassword) @@ -288,6 +313,8 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("pdns-server", "When using the PowerDNS/PDNS provider, specify the URL to the pdns server (required when --provider=pdns)").Default(defaultConfig.PDNSServer).StringVar(&cfg.PDNSServer) app.Flag("pdns-api-key", "When using the PowerDNS/PDNS provider, specify the API key to use to authorize requests (required when --provider=pdns)").Default(defaultConfig.PDNSAPIKey).StringVar(&cfg.PDNSAPIKey) app.Flag("pdns-tls-enabled", "When using the PowerDNS/PDNS provider, specify whether to use TLS (default: false, requires --tls-ca, optionally specify --tls-client-cert and --tls-client-cert-key)").Default(strconv.FormatBool(defaultConfig.PDNSTLSEnabled)).BoolVar(&cfg.PDNSTLSEnabled) + app.Flag("ns1-endpoint", "When using the NS1 provider, specify the URL of the API endpoint to target (default: https://api.nsone.net/v1/)").Default(defaultConfig.NS1Endpoint).StringVar(&cfg.NS1Endpoint) + app.Flag("ns1-ignoressl", "When using the NS1 provider, specify whether to verify the SSL certificate (default: false)").Default(strconv.FormatBool(defaultConfig.NS1IgnoreSSL)).BoolVar(&cfg.NS1IgnoreSSL) // Flags related to TLS communication app.Flag("tls-ca", "When using TLS communication, the path to the certificate authority to verify server communications (optionally specify --tls-client-cert for two-way TLS)").Default(defaultConfig.TLSCA).StringVar(&cfg.TLSCA) @@ -308,6 +335,10 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("rfc2136-tsig-secret-alg", "When using the RFC2136 provider, specify the TSIG (base64) value to attached to DNS messages (required when --rfc2136-insecure=false)").Default(defaultConfig.RFC2136TSIGSecretAlg).StringVar(&cfg.RFC2136TSIGSecretAlg) app.Flag("rfc2136-tsig-axfr", "When using the RFC2136 provider, specify the TSIG (base64) value to attached to DNS messages (required when --rfc2136-insecure=false)").BoolVar(&cfg.RFC2136TAXFR) + // Flags related to TransIP provider + app.Flag("transip-account", "When using the TransIP provider, specify the account name (required when --provider=transip)").Default(defaultConfig.TransIPAccountName).StringVar(&cfg.TransIPAccountName) + app.Flag("transip-keyfile", "When using the TransIP provider, specify the path to the private key file (required when --provider=transip)").Default(defaultConfig.TransIPPrivateKeyFile).StringVar(&cfg.TransIPPrivateKeyFile) + // Flags related to policies app.Flag("policy", "Modify how DNS records are synchronized between sources and providers (default: sync, options: sync, upsert-only)").Default(defaultConfig.Policy).EnumVar(&cfg.Policy, "sync", "upsert-only") diff --git a/pkg/apis/externaldns/types_test.go b/pkg/apis/externaldns/types_test.go index ab474b498..dc580a036 100644 --- a/pkg/apis/externaldns/types_test.go +++ b/pkg/apis/externaldns/types_test.go @@ -40,6 +40,7 @@ var ( Provider: "google", GoogleProject: "", DomainFilter: []string{""}, + ExcludeDomains: []string{""}, ZoneIDFilter: []string{""}, AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json", AWSZoneType: "", @@ -60,6 +61,7 @@ var ( InfobloxWapiVersion: "2.3.1", InfobloxView: "", InfobloxSSLVerify: true, + InfobloxMaxResults: 0, OCIConfigFile: "/etc/kubernetes/oci.yaml", InMemoryZones: []string{""}, PDNSServer: "http://localhost:8081", @@ -82,6 +84,8 @@ var ( CRDSourceAPIVersion: "externaldns.k8s.io/v1alpha1", CRDSourceKind: "DNSEndpoint", RcodezeroTXTEncrypt: false, + TransIPAccountName: "", + TransIPPrivateKeyFile: "", } overriddenConfig = &Config{ @@ -97,6 +101,7 @@ var ( Provider: "google", GoogleProject: "project", DomainFilter: []string{"example.org", "company.com"}, + ExcludeDomains: []string{"xapi.example.org", "xapi.company.com"}, ZoneIDFilter: []string{"/hostedzone/ZTST1", "/hostedzone/ZTST2"}, AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json", AWSZoneType: "private", @@ -117,6 +122,7 @@ var ( InfobloxWapiVersion: "2.6.1", InfobloxView: "internal", InfobloxSSLVerify: false, + InfobloxMaxResults: 2000, OCIConfigFile: "oci.yaml", InMemoryZones: []string{"example.org", "company.com"}, PDNSServer: "http://ns.example.com:8081", @@ -143,6 +149,10 @@ var ( CRDSourceAPIVersion: "test.k8s.io/v1alpha1", CRDSourceKind: "Endpoint", RcodezeroTXTEncrypt: true, + NS1Endpoint: "https://api.example.com/v1", + NS1IgnoreSSL: true, + TransIPAccountName: "transip", + TransIPPrivateKeyFile: "/path/to/transip.key", } // minimal config with istio gateway source and multiple ingressgateway load balancer services @@ -158,6 +168,7 @@ var ( Provider: "google", GoogleProject: "", DomainFilter: []string{""}, + ExcludeDomains: []string{""}, ZoneIDFilter: []string{""}, AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json", AWSZoneType: "", @@ -245,6 +256,7 @@ func TestParseFlags(t *testing.T) { "--infoblox-wapi-password=infoblox", "--infoblox-wapi-version=2.6.1", "--infoblox-view=internal", + "--infoblox-max-results=2000", "--inmemory-zone=example.org", "--inmemory-zone=company.com", "--pdns-server=http://ns.example.com:8081", @@ -257,6 +269,8 @@ func TestParseFlags(t *testing.T) { "--no-infoblox-ssl-verify", "--domain-filter=example.org", "--domain-filter=company.com", + "--exclude-domains=xapi.example.org", + "--exclude-domains=xapi.company.com", "--zone-id-filter=/hostedzone/ZTST1", "--zone-id-filter=/hostedzone/ZTST2", "--aws-zone-type=private", @@ -284,6 +298,10 @@ func TestParseFlags(t *testing.T) { "--crd-source-apiversion=test.k8s.io/v1alpha1", "--crd-source-kind=Endpoint", "--rcodezero-txt-encrypt", + "--ns1-endpoint=https://api.example.com/v1", + "--ns1-ignoressl", + "--transip-account=transip", + "--transip-keyfile=/path/to/transip.key", }, envVars: map[string]string{}, expected: overriddenConfig, @@ -314,9 +332,11 @@ func TestParseFlags(t *testing.T) { "EXTERNAL_DNS_INFOBLOX_WAPI_VERSION": "2.6.1", "EXTERNAL_DNS_INFOBLOX_VIEW": "internal", "EXTERNAL_DNS_INFOBLOX_SSL_VERIFY": "0", + "EXTERNAL_DNS_INFOBLOX_MAX_RESULTS": "2000", "EXTERNAL_DNS_OCI_CONFIG_FILE": "oci.yaml", "EXTERNAL_DNS_INMEMORY_ZONE": "example.org\ncompany.com", "EXTERNAL_DNS_DOMAIN_FILTER": "example.org\ncompany.com", + "EXTERNAL_DNS_EXCLUDE_DOMAINS": "xapi.example.org\nxapi.company.com", "EXTERNAL_DNS_PDNS_SERVER": "http://ns.example.com:8081", "EXTERNAL_DNS_PDNS_API_KEY": "some-secret-key", "EXTERNAL_DNS_PDNS_TLS_ENABLED": "1", @@ -349,6 +369,10 @@ func TestParseFlags(t *testing.T) { "EXTERNAL_DNS_CRD_SOURCE_APIVERSION": "test.k8s.io/v1alpha1", "EXTERNAL_DNS_CRD_SOURCE_KIND": "Endpoint", "EXTERNAL_DNS_RCODEZERO_TXT_ENCRYPT": "1", + "EXTERNAL_DNS_NS1_ENDPOINT": "https://api.example.com/v1", + "EXTERNAL_DNS_NS1_IGNORESSL": "1", + "EXTERNAL_DNS_TRANSIP_ACCOUNT": "transip", + "EXTERNAL_DNS_TRANSIP_KEYFILE": "/path/to/transip.key", }, expected: overriddenConfig, }, diff --git a/provider/alibaba_cloud.go b/provider/alibaba_cloud.go index a0b66734b..6a06949ce 100644 --- a/provider/alibaba_cloud.go +++ b/provider/alibaba_cloud.go @@ -17,6 +17,7 @@ limitations under the License. package provider import ( + "context" "fmt" "io/ioutil" "strings" @@ -291,7 +292,7 @@ func (p *AlibabaCloudProvider) Records() (endpoints []*endpoint.Endpoint, err er // ApplyChanges applies the given changes. // // Returns nil if the operation was successful or an error if the operation failed. -func (p *AlibabaCloudProvider) ApplyChanges(changes *plan.Changes) error { +func (p *AlibabaCloudProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { if changes == nil || len(changes.Create)+len(changes.Delete)+len(changes.UpdateNew) == 0 { // No op return nil diff --git a/provider/alibaba_cloud_test.go b/provider/alibaba_cloud_test.go index defbfad1b..4e86dc537 100644 --- a/provider/alibaba_cloud_test.go +++ b/provider/alibaba_cloud_test.go @@ -17,6 +17,7 @@ limitations under the License. package provider import ( + "context" "testing" "github.com/aliyun/alibaba-cloud-sdk-go/services/alidns" @@ -301,7 +302,7 @@ func TestAlibabaCloudProvider_ApplyChanges(t *testing.T) { }, }, } - p.ApplyChanges(&changes) + p.ApplyChanges(context.Background(), &changes) endpoints, err := p.Records() if err != nil { t.Errorf("Failed to get records: %v", err) @@ -358,7 +359,7 @@ func TestAlibabaCloudProvider_ApplyChanges_PrivateZone(t *testing.T) { }, }, } - p.ApplyChanges(&changes) + p.ApplyChanges(context.Background(), &changes) endpoints, err := p.Records() if err != nil { t.Errorf("Failed to get records: %v", err) diff --git a/provider/aws.go b/provider/aws.go index 37d758768..baabba378 100644 --- a/provider/aws.go +++ b/provider/aws.go @@ -17,6 +17,7 @@ limitations under the License. package provider import ( + "context" "fmt" "sort" "strings" @@ -319,15 +320,19 @@ func (p *AWSProvider) doRecords(action string, endpoints []*endpoint.Endpoint) e } // ApplyChanges applies a given set of changes in a given zone. -func (p *AWSProvider) ApplyChanges(changes *plan.Changes) error { +func (p *AWSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { zones, err := p.Zones() if err != nil { return err } - records, err := p.records(zones) - if err != nil { - log.Errorf("getting records failed: %v", err) + records, ok := ctx.Value(RecordsContextKey).([]*endpoint.Endpoint) + if !ok { + var err error + records, err = p.records(zones) + if err != nil { + log.Errorf("getting records failed: %v", err) + } } combinedChanges := make([]*route53.Change, 0, len(changes.Create)+len(changes.UpdateNew)+len(changes.Delete)) diff --git a/provider/aws_sd.go b/provider/aws_sd.go index 2bd74a132..8f921c7ef 100644 --- a/provider/aws_sd.go +++ b/provider/aws_sd.go @@ -17,6 +17,7 @@ limitations under the License. package provider import ( + "context" "strings" "crypto/sha256" @@ -193,7 +194,7 @@ func (p *AWSSDProvider) instancesToEndpoint(ns *sd.NamespaceSummary, srv *sd.Ser } // ApplyChanges applies Kubernetes changes in endpoints to AWS API -func (p *AWSSDProvider) ApplyChanges(changes *plan.Changes) error { +func (p *AWSSDProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { // return early if there is nothing to change if len(changes.Create) == 0 && len(changes.Delete) == 0 && len(changes.UpdateNew) == 0 { log.Info("All records are already up to date") diff --git a/provider/aws_sd_test.go b/provider/aws_sd_test.go index c567094fb..f25130fc9 100644 --- a/provider/aws_sd_test.go +++ b/provider/aws_sd_test.go @@ -17,6 +17,7 @@ limitations under the License. package provider import ( + "context" "errors" "math/rand" "reflect" @@ -316,7 +317,7 @@ func TestAWSSDProvider_ApplyChanges(t *testing.T) { provider := newTestAWSSDProvider(api, NewDomainFilter([]string{}), "") // apply creates - provider.ApplyChanges(&plan.Changes{ + provider.ApplyChanges(context.Background(), &plan.Changes{ Create: expectedEndpoints, }) @@ -332,7 +333,7 @@ func TestAWSSDProvider_ApplyChanges(t *testing.T) { assert.True(t, testutils.SameEndpoints(expectedEndpoints, endpoints), "expected and actual endpoints don't match, expected=%v, actual=%v", expectedEndpoints, endpoints) // apply deletes - provider.ApplyChanges(&plan.Changes{ + provider.ApplyChanges(context.Background(), &plan.Changes{ Delete: expectedEndpoints, }) diff --git a/provider/aws_test.go b/provider/aws_test.go index 2dcec4d80..8802054c7 100644 --- a/provider/aws_test.go +++ b/provider/aws_test.go @@ -17,6 +17,7 @@ limitations under the License. package provider import ( + "context" "fmt" "net" "sort" @@ -412,79 +413,96 @@ func TestAWSDeleteRecords(t *testing.T) { } func TestAWSApplyChanges(t *testing.T) { - provider, _ := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{ - endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"), - endpoint.NewEndpointWithTTL("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"), - endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.4.4"), - endpoint.NewEndpointWithTTL("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.4.4"), - endpoint.NewEndpointWithTTL("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "bar.elb.amazonaws.com"), - endpoint.NewEndpointWithTTL("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "qux.elb.amazonaws.com"), - endpoint.NewEndpointWithTTL("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "bar.elb.amazonaws.com"), - endpoint.NewEndpointWithTTL("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "qux.elb.amazonaws.com"), - endpoint.NewEndpointWithTTL("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8", "8.8.4.4"), - endpoint.NewEndpointWithTTL("delete-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4", "4.3.2.1"), - }) - - createRecords := []*endpoint.Endpoint{ - endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"), - endpoint.NewEndpoint("create-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.4.4"), - endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"), - endpoint.NewEndpoint("create-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"), - endpoint.NewEndpoint("create-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8", "8.8.4.4"), + tests := []struct { + name string + setup func(p *AWSProvider) context.Context + listRRSets int + }{ + {"no cache", func(p *AWSProvider) context.Context { return context.Background() }, 3}, + {"cached", func(p *AWSProvider) context.Context { + records, err := p.Records() + require.NoError(t, err) + return context.WithValue(context.Background(), RecordsContextKey, records) + }, 0}, } - currentRecords := []*endpoint.Endpoint{ - endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"), - endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.4.4"), - endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "bar.elb.amazonaws.com"), - endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "bar.elb.amazonaws.com"), - endpoint.NewEndpoint("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8", "8.8.4.4"), + for _, tt := range tests { + provider, _ := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{ + endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"), + endpoint.NewEndpointWithTTL("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"), + endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.4.4"), + endpoint.NewEndpointWithTTL("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.4.4"), + endpoint.NewEndpointWithTTL("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "bar.elb.amazonaws.com"), + endpoint.NewEndpointWithTTL("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "qux.elb.amazonaws.com"), + endpoint.NewEndpointWithTTL("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "bar.elb.amazonaws.com"), + endpoint.NewEndpointWithTTL("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "qux.elb.amazonaws.com"), + endpoint.NewEndpointWithTTL("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8", "8.8.4.4"), + endpoint.NewEndpointWithTTL("delete-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4", "4.3.2.1"), + }) + + createRecords := []*endpoint.Endpoint{ + endpoint.NewEndpoint("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"), + endpoint.NewEndpoint("create-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.4.4"), + endpoint.NewEndpoint("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"), + endpoint.NewEndpoint("create-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.elb.amazonaws.com"), + endpoint.NewEndpoint("create-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8", "8.8.4.4"), + } + + currentRecords := []*endpoint.Endpoint{ + endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"), + endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.4.4"), + endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "bar.elb.amazonaws.com"), + endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "bar.elb.amazonaws.com"), + endpoint.NewEndpoint("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8", "8.8.4.4"), + } + updatedRecords := []*endpoint.Endpoint{ + endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4"), + endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "4.3.2.1"), + endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "baz.elb.amazonaws.com"), + endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "baz.elb.amazonaws.com"), + endpoint.NewEndpoint("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4", "4.3.2.1"), + } + + deleteRecords := []*endpoint.Endpoint{ + endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"), + endpoint.NewEndpoint("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.4.4"), + endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "qux.elb.amazonaws.com"), + endpoint.NewEndpoint("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "qux.elb.amazonaws.com"), + endpoint.NewEndpoint("delete-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4", "4.3.2.1"), + } + + changes := &plan.Changes{ + Create: createRecords, + UpdateNew: updatedRecords, + UpdateOld: currentRecords, + Delete: deleteRecords, + } + + ctx := tt.setup(provider) + + counter := NewRoute53APICounter(provider.client) + provider.client = counter + require.NoError(t, provider.ApplyChanges(ctx, changes)) + + assert.Equal(t, 1, counter.calls["ListHostedZonesPages"], tt.name) + assert.Equal(t, tt.listRRSets, counter.calls["ListResourceRecordSetsPages"], tt.name) + + records, err := provider.Records() + require.NoError(t, err, tt.name) + + validateEndpoints(t, records, []*endpoint.Endpoint{ + endpoint.NewEndpointWithTTL("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"), + endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4"), + endpoint.NewEndpointWithTTL("create-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.4.4"), + endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "4.3.2.1"), + endpoint.NewEndpointWithTTL("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.elb.amazonaws.com"), + endpoint.NewEndpointWithTTL("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "baz.elb.amazonaws.com"), + endpoint.NewEndpointWithTTL("create-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.elb.amazonaws.com"), + endpoint.NewEndpointWithTTL("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "baz.elb.amazonaws.com"), + endpoint.NewEndpointWithTTL("create-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8", "8.8.4.4"), + endpoint.NewEndpointWithTTL("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4", "4.3.2.1"), + }) } - updatedRecords := []*endpoint.Endpoint{ - endpoint.NewEndpoint("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4"), - endpoint.NewEndpoint("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "4.3.2.1"), - endpoint.NewEndpoint("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "baz.elb.amazonaws.com"), - endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "baz.elb.amazonaws.com"), - endpoint.NewEndpoint("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4", "4.3.2.1"), - } - - deleteRecords := []*endpoint.Endpoint{ - endpoint.NewEndpoint("delete-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"), - endpoint.NewEndpoint("delete-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.4.4"), - endpoint.NewEndpoint("delete-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "qux.elb.amazonaws.com"), - endpoint.NewEndpoint("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "qux.elb.amazonaws.com"), - endpoint.NewEndpoint("delete-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4", "4.3.2.1"), - } - - changes := &plan.Changes{ - Create: createRecords, - UpdateNew: updatedRecords, - UpdateOld: currentRecords, - Delete: deleteRecords, - } - - counter := NewRoute53APICounter(provider.client) - provider.client = counter - require.NoError(t, provider.ApplyChanges(changes)) - - assert.Equal(t, 1, counter.calls["ListHostedZonesPages"]) - assert.Equal(t, 3, counter.calls["ListResourceRecordSetsPages"]) - - records, err := provider.Records() - require.NoError(t, err) - - validateEndpoints(t, records, []*endpoint.Endpoint{ - endpoint.NewEndpointWithTTL("create-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"), - endpoint.NewEndpointWithTTL("update-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4"), - endpoint.NewEndpointWithTTL("create-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.4.4"), - endpoint.NewEndpointWithTTL("update-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "4.3.2.1"), - endpoint.NewEndpointWithTTL("create-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.elb.amazonaws.com"), - endpoint.NewEndpointWithTTL("update-test-cname.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "baz.elb.amazonaws.com"), - endpoint.NewEndpointWithTTL("create-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.elb.amazonaws.com"), - endpoint.NewEndpointWithTTL("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "baz.elb.amazonaws.com"), - endpoint.NewEndpointWithTTL("create-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8", "8.8.4.4"), - endpoint.NewEndpointWithTTL("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4", "4.3.2.1"), - }) } func TestAWSApplyChangesDryRun(t *testing.T) { @@ -541,7 +559,7 @@ func TestAWSApplyChangesDryRun(t *testing.T) { Delete: deleteRecords, } - require.NoError(t, provider.ApplyChanges(changes)) + require.NoError(t, provider.ApplyChanges(context.Background(), changes)) records, err := provider.Records() require.NoError(t, err) diff --git a/provider/azure.go b/provider/azure.go index 900262048..3f887e555 100644 --- a/provider/azure.go +++ b/provider/azure.go @@ -17,6 +17,7 @@ limitations under the License. package provider import ( + "context" "fmt" "io/ioutil" "strings" @@ -209,7 +210,7 @@ func (p *AzureProvider) Records() (endpoints []*endpoint.Endpoint, _ error) { // ApplyChanges applies the given changes. // // Returns nil if the operation was successful or an error if the operation failed. -func (p *AzureProvider) ApplyChanges(changes *plan.Changes) error { +func (p *AzureProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { zones, err := p.zones() if err != nil { return err diff --git a/provider/azure_test.go b/provider/azure_test.go index 37e5fb138..36d697661 100644 --- a/provider/azure_test.go +++ b/provider/azure_test.go @@ -17,6 +17,7 @@ limitations under the License. package provider import ( + "context" "testing" "github.com/Azure/azure-sdk-for-go/arm/dns" @@ -344,7 +345,7 @@ func testAzureApplyChangesInternal(t *testing.T, dryRun bool, client RecordsClie Delete: deleteRecords, } - if err := provider.ApplyChanges(changes); err != nil { + if err := provider.ApplyChanges(context.Background(), changes); err != nil { t.Fatal(err) } } diff --git a/provider/cloudflare.go b/provider/cloudflare.go index a7705d0bd..cecb810c3 100644 --- a/provider/cloudflare.go +++ b/provider/cloudflare.go @@ -181,18 +181,17 @@ func (p *CloudFlareProvider) Records() ([]*endpoint.Endpoint, error) { return nil, err } - for _, r := range records { - if supportedRecordType(r.Type) { - endpoints = append(endpoints, endpoint.NewEndpointWithTTL(r.Name, r.Type, endpoint.TTL(r.TTL), r.Content).WithProviderSpecific(source.CloudflareProxiedKey, strconv.FormatBool(r.Proxied))) - } - } + // As CloudFlare does not support "sets" of targets, but instead returns + // a single entry for each name/type/target, we have to group by name + // and record to allow the planner to calculate the correct plan. See #992. + endpoints = append(endpoints, groupByNameAndType(records)...) } return endpoints, nil } // ApplyChanges applies a given set of changes in a given zone. -func (p *CloudFlareProvider) ApplyChanges(changes *plan.Changes) error { +func (p *CloudFlareProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { proxiedByDefault := p.proxiedByDefault combinedChanges := make([]*cloudFlareChange, 0, len(changes.Create)+len(changes.UpdateNew)+len(changes.Delete)) @@ -354,3 +353,40 @@ func shouldBeProxied(endpoint *endpoint.Endpoint, proxiedByDefault bool) bool { } return proxied } + +func groupByNameAndType(records []cloudflare.DNSRecord) []*endpoint.Endpoint { + endpoints := []*endpoint.Endpoint{} + + // group supported records by name and type + groups := map[string][]cloudflare.DNSRecord{} + + for _, r := range records { + if !supportedRecordType(r.Type) { + continue + } + + groupBy := r.Name + r.Type + if _, ok := groups[groupBy]; !ok { + groups[groupBy] = []cloudflare.DNSRecord{} + } + + groups[groupBy] = append(groups[groupBy], r) + } + + // create single endpoint with all the targets for each name/type + for _, records := range groups { + targets := make([]string, len(records)) + for i, record := range records { + targets[i] = record.Content + } + endpoints = append(endpoints, + endpoint.NewEndpointWithTTL( + records[0].Name, + records[0].Type, + endpoint.TTL(records[0].TTL), + targets...). + WithProviderSpecific(source.CloudflareProxiedKey, strconv.FormatBool(records[0].Proxied))) + } + + return endpoints +} diff --git a/provider/cloudflare_test.go b/provider/cloudflare_test.go index e67d4a7bd..1c3b68854 100644 --- a/provider/cloudflare_test.go +++ b/provider/cloudflare_test.go @@ -542,7 +542,7 @@ func TestApplyChanges(t *testing.T) { changes.Delete = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test.zalando.to.", Targets: endpoint.Targets{"target"}}} changes.UpdateOld = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test.zalando.to.", Targets: endpoint.Targets{"target-old"}}} changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test.zalando.to.", Targets: endpoint.Targets{"target-new"}}} - err := provider.ApplyChanges(changes) + err := provider.ApplyChanges(context.Background(), changes) if err != nil { t.Errorf("should not fail, %s", err) } @@ -553,7 +553,7 @@ func TestApplyChanges(t *testing.T) { changes.UpdateOld = []*endpoint.Endpoint{} changes.UpdateNew = []*endpoint.Endpoint{} - err = provider.ApplyChanges(changes) + err = provider.ApplyChanges(context.Background(), changes) if err != nil { t.Errorf("should not fail, %s", err) } @@ -595,3 +595,225 @@ func validateCloudFlareZones(t *testing.T, zones []cloudflare.Zone, expected []c assert.Equal(t, expected[i].Name, zone.Name) } } + +func TestGroupByNameAndType(t *testing.T) { + testCases := []struct { + Name string + Records []cloudflare.DNSRecord + ExpectedEndpoints []*endpoint.Endpoint + }{ + { + Name: "empty", + Records: []cloudflare.DNSRecord{}, + ExpectedEndpoints: []*endpoint.Endpoint{}, + }, + { + Name: "single record - single target", + Records: []cloudflare.DNSRecord{ + { + Name: "foo.com", + Type: endpoint.RecordTypeA, + Content: "10.10.10.1", + TTL: defaultCloudFlareRecordTTL, + }, + }, + ExpectedEndpoints: []*endpoint.Endpoint{ + { + DNSName: "foo.com", + Targets: endpoint.Targets{"10.10.10.1"}, + RecordType: endpoint.RecordTypeA, + RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL), + Labels: endpoint.Labels{}, + ProviderSpecific: endpoint.ProviderSpecific{ + { + Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", + Value: "false", + }, + }, + }, + }, + }, + { + Name: "single record - multiple targets", + Records: []cloudflare.DNSRecord{ + { + Name: "foo.com", + Type: endpoint.RecordTypeA, + Content: "10.10.10.1", + TTL: defaultCloudFlareRecordTTL, + }, + { + Name: "foo.com", + Type: endpoint.RecordTypeA, + Content: "10.10.10.2", + TTL: defaultCloudFlareRecordTTL, + }, + }, + ExpectedEndpoints: []*endpoint.Endpoint{ + { + DNSName: "foo.com", + Targets: endpoint.Targets{"10.10.10.1", "10.10.10.2"}, + RecordType: endpoint.RecordTypeA, + RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL), + Labels: endpoint.Labels{}, + ProviderSpecific: endpoint.ProviderSpecific{ + { + Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", + Value: "false", + }, + }, + }, + }, + }, + { + Name: "multiple record - multiple targets", + Records: []cloudflare.DNSRecord{ + { + Name: "foo.com", + Type: endpoint.RecordTypeA, + Content: "10.10.10.1", + TTL: defaultCloudFlareRecordTTL, + }, + { + Name: "foo.com", + Type: endpoint.RecordTypeA, + Content: "10.10.10.2", + TTL: defaultCloudFlareRecordTTL, + }, + { + Name: "bar.de", + Type: endpoint.RecordTypeA, + Content: "10.10.10.1", + TTL: defaultCloudFlareRecordTTL, + }, + { + Name: "bar.de", + Type: endpoint.RecordTypeA, + Content: "10.10.10.2", + TTL: defaultCloudFlareRecordTTL, + }, + }, + ExpectedEndpoints: []*endpoint.Endpoint{ + { + DNSName: "foo.com", + Targets: endpoint.Targets{"10.10.10.1", "10.10.10.2"}, + RecordType: endpoint.RecordTypeA, + RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL), + Labels: endpoint.Labels{}, + ProviderSpecific: endpoint.ProviderSpecific{ + { + Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", + Value: "false", + }, + }, + }, + { + DNSName: "bar.de", + Targets: endpoint.Targets{"10.10.10.1", "10.10.10.2"}, + RecordType: endpoint.RecordTypeA, + RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL), + Labels: endpoint.Labels{}, + ProviderSpecific: endpoint.ProviderSpecific{ + { + Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", + Value: "false", + }, + }, + }, + }, + }, + { + Name: "multiple record - mixed single/multiple targets", + Records: []cloudflare.DNSRecord{ + { + Name: "foo.com", + Type: endpoint.RecordTypeA, + Content: "10.10.10.1", + TTL: defaultCloudFlareRecordTTL, + }, + { + Name: "foo.com", + Type: endpoint.RecordTypeA, + Content: "10.10.10.2", + TTL: defaultCloudFlareRecordTTL, + }, + { + Name: "bar.de", + Type: endpoint.RecordTypeA, + Content: "10.10.10.1", + TTL: defaultCloudFlareRecordTTL, + }, + }, + ExpectedEndpoints: []*endpoint.Endpoint{ + { + DNSName: "foo.com", + Targets: endpoint.Targets{"10.10.10.1", "10.10.10.2"}, + RecordType: endpoint.RecordTypeA, + RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL), + Labels: endpoint.Labels{}, + ProviderSpecific: endpoint.ProviderSpecific{ + { + Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", + Value: "false", + }, + }, + }, + { + DNSName: "bar.de", + Targets: endpoint.Targets{"10.10.10.1"}, + RecordType: endpoint.RecordTypeA, + RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL), + Labels: endpoint.Labels{}, + ProviderSpecific: endpoint.ProviderSpecific{ + { + Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", + Value: "false", + }, + }, + }, + }, + }, + { + Name: "unsupported record type", + Records: []cloudflare.DNSRecord{ + { + Name: "foo.com", + Type: endpoint.RecordTypeA, + Content: "10.10.10.1", + TTL: defaultCloudFlareRecordTTL, + }, + { + Name: "foo.com", + Type: endpoint.RecordTypeA, + Content: "10.10.10.2", + TTL: defaultCloudFlareRecordTTL, + }, + { + Name: "bar.de", + Type: "NOT SUPPORTED", + Content: "10.10.10.1", + TTL: defaultCloudFlareRecordTTL, + }, + }, + ExpectedEndpoints: []*endpoint.Endpoint{ + { + DNSName: "foo.com", + Targets: endpoint.Targets{"10.10.10.1", "10.10.10.2"}, + RecordType: endpoint.RecordTypeA, + RecordTTL: endpoint.TTL(defaultCloudFlareRecordTTL), + Labels: endpoint.Labels{}, + ProviderSpecific: endpoint.ProviderSpecific{ + { + Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", + Value: "false", + }, + }, + }, + }, + }, + } + + for _, tc := range testCases { + assert.Equal(t, groupByNameAndType(tc.Records), tc.ExpectedEndpoints) + } +} diff --git a/provider/coredns.go b/provider/coredns.go index 4e78bdbf4..bd09f40a1 100644 --- a/provider/coredns.go +++ b/provider/coredns.go @@ -298,7 +298,7 @@ func (p coreDNSProvider) Records() ([]*endpoint.Endpoint, error) { } // ApplyChanges stores changes back to etcd converting them to CoreDNS format and aggregating A/CNAME and TXT records -func (p coreDNSProvider) ApplyChanges(changes *plan.Changes) error { +func (p coreDNSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { grouped := map[string][]*endpoint.Endpoint{} for _, ep := range changes.Create { grouped[ep.DNSName] = append(grouped[ep.DNSName], ep) diff --git a/provider/coredns_test.go b/provider/coredns_test.go index 147711743..698207a0d 100644 --- a/provider/coredns_test.go +++ b/provider/coredns_test.go @@ -17,6 +17,7 @@ limitations under the License. package provider import ( + "context" "strings" "testing" @@ -227,7 +228,7 @@ func TestCoreDNSApplyChanges(t *testing.T) { endpoint.NewEndpoint("domain2.local", endpoint.RecordTypeCNAME, "site.local"), }, } - coredns.ApplyChanges(changes1) + coredns.ApplyChanges(context.Background(), changes1) expectedServices1 := map[string]*Service{ "/skydns/local/domain1": {Host: "5.5.5.5", Text: "string1"}, @@ -285,7 +286,7 @@ func applyServiceChanges(provider coreDNSProvider, changes *plan.Changes) { } } } - provider.ApplyChanges(changes) + provider.ApplyChanges(context.Background(), changes) } func validateServices(services, expectedServices map[string]*Service, t *testing.T, step int) { diff --git a/provider/designate.go b/provider/designate.go index 48e8ce8a9..50ac157b7 100644 --- a/provider/designate.go +++ b/provider/designate.go @@ -17,6 +17,7 @@ limitations under the License. package provider import ( + "context" "fmt" "net" "net/http" @@ -379,7 +380,7 @@ func addEndpoint(ep *endpoint.Endpoint, recordSets map[string]*recordSet, delete } // ApplyChanges applies a given set of changes in a given zone. -func (p designateProvider) ApplyChanges(changes *plan.Changes) error { +func (p designateProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { managedZones, err := p.getZones() if err != nil { return err diff --git a/provider/designate_test.go b/provider/designate_test.go index db060b92f..3753ed303 100644 --- a/provider/designate_test.go +++ b/provider/designate_test.go @@ -17,6 +17,7 @@ limitations under the License. package provider import ( + "context" "encoding/pem" "fmt" "io/ioutil" @@ -407,7 +408,7 @@ func testDesignateCreateRecords(t *testing.T, client *fakeDesignateClient) []*re expectedCopy := make([]*recordsets.RecordSet, len(expected)) copy(expectedCopy, expected) - err := client.ToProvider().ApplyChanges(&plan.Changes{Create: endpoints}) + err := client.ToProvider().ApplyChanges(context.Background(), &plan.Changes{Create: endpoints}) if err != nil { t.Fatal(err) } @@ -495,7 +496,7 @@ func testDesignateUpdateRecords(t *testing.T, client *fakeDesignateClient) []*re expected[2].Records = []string{"10.3.3.1"} expected[3].Records = []string{"10.2.1.1", "10.3.3.2"} - err := client.ToProvider().ApplyChanges(&plan.Changes{UpdateOld: updatesOld, UpdateNew: updatesNew}) + err := client.ToProvider().ApplyChanges(context.Background(), &plan.Changes{UpdateOld: updatesOld, UpdateNew: updatesNew}) if err != nil { t.Fatal(err) } @@ -553,7 +554,7 @@ func testDesignateDeleteRecords(t *testing.T, client *fakeDesignateClient) { expected[3].Records = []string{"10.3.3.2"} expected = expected[1:] - err := client.ToProvider().ApplyChanges(&plan.Changes{Delete: deletes}) + err := client.ToProvider().ApplyChanges(context.Background(), &plan.Changes{Delete: deletes}) if err != nil { t.Fatal(err) } diff --git a/provider/digital_ocean.go b/provider/digital_ocean.go index 590fa5e4b..00daf60fd 100644 --- a/provider/digital_ocean.go +++ b/provider/digital_ocean.go @@ -17,6 +17,7 @@ limitations under the License. package provider import ( + goctx "context" "fmt" "os" "strings" @@ -261,7 +262,7 @@ func (p *DigitalOceanProvider) submitChanges(changes []*DigitalOceanChange) erro } // ApplyChanges applies a given set of changes in a given zone. -func (p *DigitalOceanProvider) ApplyChanges(changes *plan.Changes) error { +func (p *DigitalOceanProvider) ApplyChanges(ctx goctx.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)...) diff --git a/provider/digital_ocean_test.go b/provider/digital_ocean_test.go index 1ce4a6dac..ab28da215 100644 --- a/provider/digital_ocean_test.go +++ b/provider/digital_ocean_test.go @@ -438,7 +438,7 @@ func TestDigitalOceanApplyChanges(t *testing.T) { changes.Delete = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test.bar.com", Targets: endpoint.Targets{"target"}}} changes.UpdateOld = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test.bar.de", Targets: endpoint.Targets{"target-old"}}} changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test.foo.com", Targets: endpoint.Targets{"target-new"}, RecordType: "CNAME", RecordTTL: 100}} - err := provider.ApplyChanges(changes) + err := provider.ApplyChanges(context.Background(), changes) if err != nil { t.Errorf("should not fail, %s", err) } diff --git a/provider/dnsimple.go b/provider/dnsimple.go index d7c7c7606..2bccfe435 100644 --- a/provider/dnsimple.go +++ b/provider/dnsimple.go @@ -17,6 +17,7 @@ limitations under the License. package provider import ( + "context" "fmt" "os" "strconv" @@ -176,7 +177,13 @@ func (p *dnsimpleProvider) Records() (endpoints []*endpoint.Endpoint, _ error) { default: continue } - endpoints = append(endpoints, endpoint.NewEndpointWithTTL(record.Name+"."+record.ZoneID, record.Type, endpoint.TTL(record.TTL), record.Content)) + // Apex records have an empty string for their name. + // Consider this when creating the endpoint dnsName + dnsName := fmt.Sprintf("%s.%s", record.Name, record.ZoneID) + if record.Name == "" { + dnsName = record.ZoneID + } + endpoints = append(endpoints, endpoint.NewEndpointWithTTL(dnsName, record.Type, endpoint.TTL(record.TTL), record.Content)) } page++ if page > records.Pagination.TotalPages { @@ -234,7 +241,12 @@ func (p *dnsimpleProvider) submitChanges(changes []*dnsimpleChange) error { log.Infof("Changing records: %s %v in zone: %s", change.Action, change.ResourceRecordSet, zone.Name) - change.ResourceRecordSet.Name = strings.TrimSuffix(change.ResourceRecordSet.Name, "."+zone.Name) + if change.ResourceRecordSet.Name == zone.Name { + change.ResourceRecordSet.Name = "" // Apex records have an empty name + } else { + change.ResourceRecordSet.Name = strings.TrimSuffix(change.ResourceRecordSet.Name, fmt.Sprintf(".%s", zone.Name)) + } + if !p.dryRun { switch change.Action { case dnsimpleCreate: @@ -321,7 +333,7 @@ func (p *dnsimpleProvider) UpdateRecords(endpoints []*endpoint.Endpoint) error { } // ApplyChanges applies a given set of changes -func (p *dnsimpleProvider) ApplyChanges(changes *plan.Changes) error { +func (p *dnsimpleProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { combinedChanges := make([]*dnsimpleChange, 0, len(changes.Create)+len(changes.UpdateNew)+len(changes.Delete)) combinedChanges = append(combinedChanges, newDnsimpleChanges(dnsimpleCreate, changes.Create)...) diff --git a/provider/dnsimple_test.go b/provider/dnsimple_test.go index b0139c4eb..1f30da028 100644 --- a/provider/dnsimple_test.go +++ b/provider/dnsimple_test.go @@ -17,6 +17,7 @@ limitations under the License. package provider import ( + "context" "fmt" "os" "testing" @@ -84,8 +85,18 @@ func TestDnsimpleServices(t *testing.T) { Priority: 0, Type: "CNAME", } + fourthRecord := dnsimple.ZoneRecord{ + ID: 4, + ZoneID: "example.com", + ParentID: 0, + Name: "", // Apex domain A record + Content: "127.0.0.1", + TTL: 3600, + Priority: 0, + Type: "A", + } - records := []dnsimple.ZoneRecord{firstRecord, secondRecord, thirdRecord} + records := []dnsimple.ZoneRecord{firstRecord, secondRecord, thirdRecord, fourthRecord} dnsimpleListRecordsResponse = dnsimple.ZoneRecordsResponse{ Response: dnsimple.Response{Pagination: &dnsimple.Pagination{}}, Data: records, @@ -115,7 +126,6 @@ func TestDnsimpleServices(t *testing.T) { mockDNS.On("CreateRecord", "1", record.ZoneID, simpleRecord).Return(&dnsimple.ZoneRecordResponse{}, nil) mockDNS.On("DeleteRecord", "1", record.ZoneID, record.ID).Return(&dnsimple.ZoneRecordResponse{}, nil) mockDNS.On("UpdateRecord", "1", record.ZoneID, record.ID, simpleRecord).Return(&dnsimple.ZoneRecordResponse{}, nil) - mockDNS.On("UpdateRecord", "1", record.ZoneID, record.ID, simpleRecord).Return(&dnsimple.ZoneRecordResponse{}, nil) } mockProvider = dnsimpleProvider{client: mockDNS} @@ -157,10 +167,13 @@ func testDnsimpleProviderApplyChanges(t *testing.T) { {DNSName: "custom-ttl.example.com", RecordTTL: 60, Targets: endpoint.Targets{"target"}, RecordType: endpoint.RecordTypeCNAME}, } changes.Delete = []*endpoint.Endpoint{{DNSName: "example-beta.example.com", Targets: endpoint.Targets{"127.0.0.1"}, RecordType: endpoint.RecordTypeA}} - changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "example.example.com", Targets: endpoint.Targets{"target"}, RecordType: endpoint.RecordTypeCNAME}} + changes.UpdateNew = []*endpoint.Endpoint{ + {DNSName: "example.example.com", Targets: endpoint.Targets{"target"}, RecordType: endpoint.RecordTypeCNAME}, + {DNSName: "example.com", Targets: endpoint.Targets{"127.0.0.1"}, RecordType: endpoint.RecordTypeA}, + } mockProvider.accountID = "1" - err := mockProvider.ApplyChanges(changes) + err := mockProvider.ApplyChanges(context.Background(), changes) if err != nil { t.Errorf("Failed to apply changes: %v", err) } @@ -173,7 +186,7 @@ func testDnsimpleProviderApplyChangesSkipsUnknown(t *testing.T) { } mockProvider.accountID = "1" - err := mockProvider.ApplyChanges(changes) + err := mockProvider.ApplyChanges(context.Background(), changes) if err != nil { t.Errorf("Failed to ignore unknown zones: %v", err) } diff --git a/provider/domain_filter.go b/provider/domain_filter.go index 4bfdef90f..c59a71c8c 100644 --- a/provider/domain_filter.go +++ b/provider/domain_filter.go @@ -23,30 +23,43 @@ import ( // DomainFilter holds a lists of valid domain names type DomainFilter struct { filters []string + exclude []string +} + +// prepareFilters provides consistent trimming for filters/exclude params +func prepareFilters(filters []string) []string { + fs := make([]string, len(filters)) + for i, domain := range filters { + fs[i] = strings.ToLower(strings.TrimSuffix(strings.TrimSpace(domain), ".")) + } + return fs +} + +// NewDomainFilterWithExclusions returns a new DomainFilter, given a list of matches and exclusions +func NewDomainFilterWithExclusions(domainFilters []string, excludeDomains []string) DomainFilter { + return DomainFilter{prepareFilters(domainFilters), prepareFilters(excludeDomains)} } // NewDomainFilter returns a new DomainFilter given a comma separated list of domains func NewDomainFilter(domainFilters []string) DomainFilter { - filters := make([]string, len(domainFilters)) - - // user can define filter domains either with trailing dot or without, we remove all trailing periods from - // the internal representation - for i, domain := range domainFilters { - filters[i] = strings.TrimSuffix(strings.TrimSpace(domain), ".") - } - - return DomainFilter{filters} + return DomainFilter{prepareFilters(domainFilters), []string{}} } // Match checks whether a domain can be found in the DomainFilter. func (df DomainFilter) Match(domain string) bool { - // return always true, if not filter is specified - if len(df.filters) == 0 { - return true + return matchFilter(df.filters, domain, true) && !matchFilter(df.exclude, domain, false) +} + +// matchFilter determines if any `filters` match `domain`. +// If no `filters` are provided, behavior depends on `emptyval` +// (empty `df.filters` matches everything, while empty `df.exclude` excludes nothing) +func matchFilter(filters []string, domain string, emptyval bool) bool { + if len(filters) == 0 { + return emptyval } - for _, filter := range df.filters { - strippedDomain := strings.TrimSuffix(domain, ".") + for _, filter := range filters { + strippedDomain := strings.ToLower(strings.TrimSuffix(domain, ".")) if filter == "" { return true @@ -60,7 +73,6 @@ func (df DomainFilter) Match(domain string) bool { return true } } - return false } diff --git a/provider/domain_filter_test.go b/provider/domain_filter_test.go index 12a562664..0edc3c392 100644 --- a/provider/domain_filter_test.go +++ b/provider/domain_filter_test.go @@ -24,6 +24,7 @@ import ( type domainFilterTest struct { domainFilter []string + exclusions []string domains []string expected bool } @@ -31,108 +32,191 @@ type domainFilterTest struct { var domainFilterTests = []domainFilterTest{ { []string{"google.com.", "exaring.de", "inovex.de"}, + []string{}, []string{"google.com", "exaring.de", "inovex.de"}, true, }, { []string{"google.com.", "exaring.de", "inovex.de"}, + []string{}, []string{"google.com", "exaring.de", "inovex.de"}, true, }, { []string{"google.com.", "exaring.de.", "inovex.de"}, + []string{}, []string{"google.com", "exaring.de", "inovex.de"}, true, }, { []string{"foo.org. "}, + []string{}, []string{"foo.org"}, true, }, { []string{" foo.org"}, + []string{}, []string{"foo.org"}, true, }, { []string{"foo.org."}, + []string{}, []string{"foo.org"}, true, }, { []string{"foo.org."}, + []string{}, []string{"baz.org"}, false, }, { []string{"baz.foo.org."}, + []string{}, []string{"foo.org"}, false, }, { []string{"", "foo.org."}, + []string{}, []string{"foo.org"}, true, }, { []string{"", "foo.org."}, []string{}, + []string{}, true, }, { []string{""}, + []string{}, []string{"foo.org"}, true, }, { []string{""}, []string{}, + []string{}, true, }, { []string{" "}, []string{}, + []string{}, true, }, { []string{"bar.sub.example.org"}, + []string{}, []string{"foo.bar.sub.example.org"}, true, }, { []string{"example.org"}, + []string{}, []string{"anexample.org", "test.anexample.org"}, false, }, { []string{".example.org"}, + []string{}, []string{"anexample.org", "test.anexample.org"}, false, }, { []string{".example.org"}, + []string{}, []string{"example.org"}, false, }, { []string{".example.org"}, + []string{}, []string{"test.example.org"}, true, }, { []string{"anexample.org"}, + []string{}, []string{"example.org", "test.example.org"}, false, }, { []string{".org"}, + []string{}, []string{"example.org", "test.example.org", "foo.test.example.org"}, true, }, + { + []string{"example.org"}, + []string{"api.example.org"}, + []string{"example.org", "test.example.org", "foo.test.example.org"}, + true, + }, + { + []string{"example.org"}, + []string{"api.example.org"}, + []string{"foo.api.example.org", "api.example.org"}, + false, + }, + { + []string{" example.org. "}, + []string{" .api.example.org "}, + []string{"foo.api.example.org", "bar.baz.api.example.org."}, + false, + }, + { + []string{"example.org."}, + []string{"api.example.org"}, + []string{"dev-api.example.org", "qa-api.example.org"}, + true, + }, + { + []string{"example.org."}, + []string{"api.example.org"}, + []string{"dev.api.example.org", "qa.api.example.org"}, + false, + }, + { + []string{"example.org", "api.example.org"}, + []string{"internal.api.example.org"}, + []string{"foo.api.example.org"}, + true, + }, + { + []string{"example.org", "api.example.org"}, + []string{"internal.api.example.org"}, + []string{"foo.internal.api.example.org"}, + false, + }, + { + []string{"eXaMPle.ORG", "API.example.ORG"}, + []string{"Foo-Bar.Example.Org"}, + []string{"FoOoo.Api.Example.Org"}, + true, + }, + { + []string{"eXaMPle.ORG", "API.example.ORG"}, + []string{"api.example.org"}, + []string{"foobar.Example.Org"}, + true, + }, + { + []string{"eXaMPle.ORG", "API.example.ORG"}, + []string{"api.example.org"}, + []string{"foobar.API.Example.Org"}, + false, + }, } func TestDomainFilterMatch(t *testing.T) { for i, tt := range domainFilterTests { + if len(tt.exclusions) > 0 { + t.Skip("NewDomainFilter() doesn't support exclusions") + } domainFilter := NewDomainFilter(tt.domainFilter) for _, domain := range tt.domains { assert.Equal(t, tt.expected, domainFilter.Match(domain), "should not fail: %v in test-case #%v", domain, i) @@ -141,6 +225,16 @@ func TestDomainFilterMatch(t *testing.T) { } } +func TestDomainFilterWithExclusions(t *testing.T) { + for i, tt := range domainFilterTests { + domainFilter := NewDomainFilterWithExclusions(tt.domainFilter, tt.exclusions) + for _, domain := range tt.domains { + assert.Equal(t, tt.expected, domainFilter.Match(domain), "should not fail: %v in test-case #%v", domain, i) + assert.Equal(t, tt.expected, domainFilter.Match(domain+"."), "should not fail: %v in test-case #%v", domain+".", i) + } + } +} + func TestDomainFilterMatchWithEmptyFilter(t *testing.T) { for _, tt := range domainFilterTests { domainFilter := DomainFilter{} @@ -150,3 +244,77 @@ func TestDomainFilterMatchWithEmptyFilter(t *testing.T) { } } } + +func TestPrepareFiltersStripsWhitespaceAndDotSuffix(t *testing.T) { + for _, tt := range []struct { + input []string + output []string + }{ + { + []string{" ", " ", ""}, + []string{"", "", ""}, + }, + { + []string{" foo ", " bar. ", "baz."}, + []string{"foo", "bar", "baz"}, + }, + { + []string{"foo.bar", " foo.bar. ", " foo.bar.baz ", " foo.bar.baz. "}, + []string{"foo.bar", "foo.bar", "foo.bar.baz", "foo.bar.baz"}, + }, + } { + t.Run("test string", func(t *testing.T) { + assert.Equal(t, tt.output, prepareFilters(tt.input)) + }) + } +} + +func TestMatchFilterReturnsProperEmptyVal(t *testing.T) { + emptyFilters := []string{} + assert.Equal(t, true, matchFilter(emptyFilters, "somedomain.com", true)) + assert.Equal(t, false, matchFilter(emptyFilters, "somedomain.com", false)) +} + +func TestDomainFilterIsConfigured(t *testing.T) { + for _, tt := range []struct { + filters []string + exclude []string + expected bool + }{ + { + []string{""}, + []string{""}, + false, + }, + { + []string{" "}, + []string{" "}, + false, + }, + { + []string{"", ""}, + []string{""}, + true, + }, + { + []string{" . "}, + []string{" . "}, + false, + }, + { + []string{" notempty.com "}, + []string{" "}, + true, + }, + { + []string{" notempty.com "}, + []string{" thisdoesntmatter.com "}, + true, + }, + } { + t.Run("test IsConfigured", func(t *testing.T) { + df := NewDomainFilterWithExclusions(tt.filters, tt.exclude) + assert.Equal(t, tt.expected, df.IsConfigured()) + }) + } +} diff --git a/provider/dyn.go b/provider/dyn.go index e34999803..52f47ac34 100644 --- a/provider/dyn.go +++ b/provider/dyn.go @@ -17,6 +17,7 @@ limitations under the License. package provider import ( + "context" "fmt" "os" "strconv" @@ -637,7 +638,7 @@ func (d *dynProviderState) Records() ([]*endpoint.Endpoint, error) { // this method does C + 2*Z requests: C=total number of changes, Z = number of // affected zones (1 login + 1 commit) -func (d *dynProviderState) ApplyChanges(changes *plan.Changes) error { +func (d *dynProviderState) ApplyChanges(ctx context.Context, changes *plan.Changes) error { log.Debugf("Processing chages: %+v", changes) if d.DryRun { diff --git a/provider/exoscale.go b/provider/exoscale.go index 4c909c1b3..be9fd8316 100644 --- a/provider/exoscale.go +++ b/provider/exoscale.go @@ -17,6 +17,7 @@ limitations under the License. package provider import ( + "context" "strings" "github.com/exoscale/egoscale" @@ -81,7 +82,7 @@ func (ep *ExoscaleProvider) getZones() (map[int64]string, error) { } // ApplyChanges simply modifies DNS via exoscale API -func (ep *ExoscaleProvider) ApplyChanges(changes *plan.Changes) error { +func (ep *ExoscaleProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { ep.OnApplyChanges(changes) if ep.dryRun { diff --git a/provider/exoscale_test.go b/provider/exoscale_test.go index 4c0c5bcbd..639040ffa 100644 --- a/provider/exoscale_test.go +++ b/provider/exoscale_test.go @@ -17,6 +17,7 @@ limitations under the License. package provider import ( + "context" "strings" "testing" @@ -173,7 +174,7 @@ func TestExoscaleApplyChanges(t *testing.T) { createExoscale = make([]createRecordExoscale, 0) deleteExoscale = make([]deleteRecordExoscale, 0) - provider.ApplyChanges(plan) + provider.ApplyChanges(context.Background(), plan) assert.Equal(t, 1, len(createExoscale)) assert.Equal(t, "foo.com", createExoscale[0].name) diff --git a/provider/google.go b/provider/google.go index 94d933349..3ba73354f 100644 --- a/provider/google.go +++ b/provider/google.go @@ -17,6 +17,7 @@ limitations under the License. package provider import ( + goctx "context" "fmt" "strings" @@ -160,7 +161,7 @@ func (p *GoogleProvider) Zones() (map[string]*dns.ManagedZone, error) { f := func(resp *dns.ManagedZonesListResponse) error { for _, zone := range resp.ManagedZones { - if p.domainFilter.Match(zone.DnsName) && p.zoneIDFilter.Match(fmt.Sprintf("%v", zone.Id)) { + if p.domainFilter.Match(zone.DnsName) && (p.zoneIDFilter.Match(fmt.Sprintf("%v", zone.Id)) || p.zoneIDFilter.Match(fmt.Sprintf("%v", zone.Name))) { zones[zone.Name] = zone log.Debugf("Matched %s (zone: %s)", zone.DnsName, zone.Name) } else { @@ -247,7 +248,7 @@ func (p *GoogleProvider) DeleteRecords(endpoints []*endpoint.Endpoint) error { } // ApplyChanges applies a given set of changes in a given zone. -func (p *GoogleProvider) ApplyChanges(changes *plan.Changes) error { +func (p *GoogleProvider) ApplyChanges(ctx goctx.Context, changes *plan.Changes) error { change := &dns.Change{} change.Additions = append(change.Additions, p.newFilteredRecords(changes.Create)...) diff --git a/provider/google_test.go b/provider/google_test.go index 8f7ea9162..a45d81cab 100644 --- a/provider/google_test.go +++ b/provider/google_test.go @@ -192,6 +192,28 @@ func hasTrailingDot(target string) bool { return strings.HasSuffix(target, ".") } +func TestGoogleZonesIDFilter(t *testing.T) { + provider := newGoogleProviderZoneOverlap(t, NewDomainFilter([]string{"cluster.local."}), NewZoneIDFilter([]string{"10002"}), false, []*endpoint.Endpoint{}) + + zones, err := provider.Zones() + require.NoError(t, err) + + validateZones(t, zones, map[string]*dns.ManagedZone{ + "internal-2": {Name: "internal-2", DnsName: "cluster.local.", Id: 10002}, + }) +} + +func TestGoogleZonesNameFilter(t *testing.T) { + provider := newGoogleProviderZoneOverlap(t, NewDomainFilter([]string{"cluster.local."}), NewZoneIDFilter([]string{"internal-2"}), false, []*endpoint.Endpoint{}) + + zones, err := provider.Zones() + require.NoError(t, err) + + validateZones(t, zones, map[string]*dns.ManagedZone{ + "internal-2": {Name: "internal-2", DnsName: "cluster.local.", Id: 10002}, + }) +} + func TestGoogleZones(t *testing.T) { provider := newGoogleProvider(t, NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), NewZoneIDFilter([]string{""}), false, []*endpoint.Endpoint{}) @@ -387,7 +409,7 @@ func TestGoogleApplyChanges(t *testing.T) { Delete: deleteRecords, } - require.NoError(t, provider.ApplyChanges(changes)) + require.NoError(t, provider.ApplyChanges(context.Background(), changes)) records, err := provider.Records() require.NoError(t, err) @@ -444,7 +466,7 @@ func TestGoogleApplyChangesDryRun(t *testing.T) { Delete: deleteRecords, } - require.NoError(t, provider.ApplyChanges(changes)) + require.NoError(t, provider.ApplyChanges(context.Background(), changes)) records, err := provider.Records() require.NoError(t, err) @@ -454,7 +476,7 @@ func TestGoogleApplyChangesDryRun(t *testing.T) { func TestGoogleApplyChangesEmpty(t *testing.T) { provider := newGoogleProvider(t, NewDomainFilter([]string{"ext-dns-test-2.gcp.zalan.do."}), NewZoneIDFilter([]string{""}), false, []*endpoint.Endpoint{}) - assert.NoError(t, provider.ApplyChanges(&plan.Changes{})) + assert.NoError(t, provider.ApplyChanges(context.Background(), &plan.Changes{})) } func TestNewFilteredRecords(t *testing.T) { @@ -562,6 +584,41 @@ func validateChangeRecord(t *testing.T, record *dns.ResourceRecordSet, expected assert.Equal(t, expected.Type, record.Type) } +func newGoogleProviderZoneOverlap(t *testing.T, domainFilter DomainFilter, zoneIDFilter ZoneIDFilter, dryRun bool, records []*endpoint.Endpoint) *GoogleProvider { + provider := &GoogleProvider{ + project: "zalando-external-dns-test", + dryRun: false, + domainFilter: domainFilter, + zoneIDFilter: zoneIDFilter, + resourceRecordSetsClient: &mockResourceRecordSetsClient{}, + managedZonesClient: &mockManagedZonesClient{}, + changesClient: &mockChangesClient{}, + } + + createZone(t, provider, &dns.ManagedZone{ + Name: "internal-1", + DnsName: "cluster.local.", + Id: 10001, + }) + + createZone(t, provider, &dns.ManagedZone{ + Name: "internal-2", + DnsName: "cluster.local.", + Id: 10002, + }) + + createZone(t, provider, &dns.ManagedZone{ + Name: "internal-3", + DnsName: "cluster.local.", + Id: 10003, + }) + + provider.dryRun = dryRun + + return provider + +} + func newGoogleProvider(t *testing.T, domainFilter DomainFilter, zoneIDFilter ZoneIDFilter, dryRun bool, records []*endpoint.Endpoint) *GoogleProvider { provider := &GoogleProvider{ project: "zalando-external-dns-test", diff --git a/provider/infoblox.go b/provider/infoblox.go index 5d2038061..d01950507 100644 --- a/provider/infoblox.go +++ b/provider/infoblox.go @@ -17,7 +17,9 @@ limitations under the License. package provider import ( + "context" "fmt" + "net/http" "os" "strconv" "strings" @@ -40,6 +42,7 @@ type InfobloxConfig struct { SSLVerify bool DryRun bool View string + MaxResults int } // InfobloxProvider implements the DNS provider for Infoblox. @@ -56,6 +59,33 @@ type infobloxRecordSet struct { res interface{} } +// MaxResultsRequestBuilder implements a HttpRequestBuilder which sets the +// _max_results query parameter on all get requests +type MaxResultsRequestBuilder struct { + maxResults int + ibclient.WapiRequestBuilder +} + +// NewMaxResultsRequestBuilder returns a MaxResultsRequestBuilder which adds +// _max_results query parameter to all GET requests +func NewMaxResultsRequestBuilder(maxResults int) *MaxResultsRequestBuilder { + return &MaxResultsRequestBuilder{ + maxResults: maxResults, + } +} + +// BuildRequest prepares the api request. it uses BuildRequest of +// WapiRequestBuilder and then add the _max_requests parameter +func (mrb *MaxResultsRequestBuilder) BuildRequest(t ibclient.RequestType, obj ibclient.IBObject, ref string, queryParams ibclient.QueryParams) (req *http.Request, err error) { + req, err = mrb.WapiRequestBuilder.BuildRequest(t, obj, ref, queryParams) + if req.Method == "GET" { + query := req.URL.Query() + query.Set("_max_results", strconv.Itoa(mrb.maxResults)) + req.URL.RawQuery = query.Encode() + } + return +} + // NewInfobloxProvider creates a new Infoblox provider. func NewInfobloxProvider(infobloxConfig InfobloxConfig) (*InfobloxProvider, error) { hostConfig := ibclient.HostConfig{ @@ -75,7 +105,15 @@ func NewInfobloxProvider(infobloxConfig InfobloxConfig) (*InfobloxProvider, erro httpPoolConnections, ) - requestBuilder := &ibclient.WapiRequestBuilder{} + var requestBuilder ibclient.HttpRequestBuilder + if infobloxConfig.MaxResults != 0 { + // use our own HttpRequestBuilder which sets _max_results paramter on GET requests + requestBuilder = NewMaxResultsRequestBuilder(infobloxConfig.MaxResults) + } else { + // use the default HttpRequestBuilder of the infoblox client + requestBuilder = &ibclient.WapiRequestBuilder{} + } + requestor := &ibclient.WapiHttpRequestor{} client, err := ibclient.NewConnector(hostConfig, transportConfig, requestBuilder, requestor) @@ -177,7 +215,7 @@ func (p *InfobloxProvider) Records() (endpoints []*endpoint.Endpoint, err error) } // ApplyChanges applies the given changes. -func (p *InfobloxProvider) ApplyChanges(changes *plan.Changes) error { +func (p *InfobloxProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { zones, err := p.zones() if err != nil { return err diff --git a/provider/infoblox_test.go b/provider/infoblox_test.go index 6ae80eb92..c36063ba8 100644 --- a/provider/infoblox_test.go +++ b/provider/infoblox_test.go @@ -17,6 +17,7 @@ limitations under the License. package provider import ( + "context" "encoding/base64" "fmt" "regexp" @@ -469,7 +470,7 @@ func testInfobloxApplyChangesInternal(t *testing.T, dryRun bool, client ibclient Delete: deleteRecords, } - if err := provider.ApplyChanges(changes); err != nil { + if err := provider.ApplyChanges(context.Background(), changes); err != nil { t.Fatal(err) } } @@ -495,3 +496,26 @@ func TestInfobloxZones(t *testing.T) { assert.Equal(t, provider.findZone(zones, "lvl2-2.lvl1-1.example.com").Fqdn, "lvl1-1.example.com") assert.Equal(t, provider.findZone(zones, "lvl2-2.lvl1-2.example.com").Fqdn, "example.com") } + +func TestMaxResultsRequestBuilder(t *testing.T) { + hostConfig := ibclient.HostConfig{ + Host: "localhost", + Port: "8080", + Username: "user", + Password: "abcd", + Version: "2.3.1", + } + + requestBuilder := NewMaxResultsRequestBuilder(54321) + requestBuilder.Init(hostConfig) + + obj := ibclient.NewRecordCNAME(ibclient.RecordCNAME{Zone: "foo.bar.com"}) + + req, _ := requestBuilder.BuildRequest(ibclient.GET, obj, "", ibclient.QueryParams{}) + + assert.True(t, req.URL.Query().Get("_max_results") == "54321") + + req, _ = requestBuilder.BuildRequest(ibclient.CREATE, obj, "", ibclient.QueryParams{}) + + assert.True(t, req.URL.Query().Get("_max_results") == "") +} diff --git a/provider/inmemory.go b/provider/inmemory.go index 1cf6c5662..790f4c470 100644 --- a/provider/inmemory.go +++ b/provider/inmemory.go @@ -17,6 +17,7 @@ limitations under the License. package provider import ( + "context" "errors" "strings" @@ -45,7 +46,7 @@ type InMemoryProvider struct { domain DomainFilter client *inMemoryClient filter *filter - OnApplyChanges func(changes *plan.Changes) + OnApplyChanges func(ctx context.Context, changes *plan.Changes) OnRecords func() } @@ -55,7 +56,7 @@ type InMemoryOption func(*InMemoryProvider) // InMemoryWithLogging injects logging when ApplyChanges is called func InMemoryWithLogging() InMemoryOption { return func(p *InMemoryProvider) { - p.OnApplyChanges = func(changes *plan.Changes) { + p.OnApplyChanges = func(ctx context.Context, changes *plan.Changes) { for _, v := range changes.Create { log.Infof("CREATE: %v", v) } @@ -94,7 +95,7 @@ func InMemoryInitZones(zones []string) InMemoryOption { func NewInMemoryProvider(opts ...InMemoryOption) *InMemoryProvider { im := &InMemoryProvider{ filter: &filter{}, - OnApplyChanges: func(changes *plan.Changes) {}, + OnApplyChanges: func(ctx context.Context, changes *plan.Changes) {}, OnRecords: func() {}, domain: NewDomainFilter([]string{""}), client: newInMemoryClient(), @@ -142,8 +143,8 @@ func (im *InMemoryProvider) Records() ([]*endpoint.Endpoint, error) { // create record - record should not exist // update/delete record - record should exist // create/update/delete lists should not have overlapping records -func (im *InMemoryProvider) ApplyChanges(changes *plan.Changes) error { - defer im.OnApplyChanges(changes) +func (im *InMemoryProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { + defer im.OnApplyChanges(ctx, changes) perZoneChanges := map[string]*plan.Changes{} @@ -188,7 +189,7 @@ func (im *InMemoryProvider) ApplyChanges(changes *plan.Changes) error { UpdateOld: convertToInMemoryRecord(perZoneChanges[zoneID].UpdateOld), Delete: convertToInMemoryRecord(perZoneChanges[zoneID].Delete), } - err := im.client.ApplyChanges(zoneID, change) + err := im.client.ApplyChanges(ctx, zoneID, change) if err != nil { return err } @@ -293,7 +294,7 @@ func (c *inMemoryClient) CreateZone(zone string) error { return nil } -func (c *inMemoryClient) ApplyChanges(zoneID string, changes *inMemoryChange) error { +func (c *inMemoryClient) ApplyChanges(ctx context.Context, zoneID string, changes *inMemoryChange) error { if err := c.validateChangeBatch(zoneID, changes); err != nil { return err } diff --git a/provider/inmemory_test.go b/provider/inmemory_test.go index 4e6190838..0d61a9480 100644 --- a/provider/inmemory_test.go +++ b/provider/inmemory_test.go @@ -17,6 +17,7 @@ limitations under the License. package provider import ( + "context" "testing" "github.com/kubernetes-incubator/external-dns/endpoint" @@ -773,7 +774,7 @@ func testInMemoryApplyChanges(t *testing.T) { c.zones = getInitData() im.client = c - err := im.ApplyChanges(ti.changes) + err := im.ApplyChanges(context.Background(), ti.changes) if ti.expectError { assert.Error(t, err) } else { diff --git a/provider/linode.go b/provider/linode.go index 7000f1961..05076133e 100644 --- a/provider/linode.go +++ b/provider/linode.go @@ -263,7 +263,7 @@ func getPriority() *int { } // ApplyChanges applies a given set of changes in a given zone. -func (p *LinodeProvider) ApplyChanges(changes *plan.Changes) error { +func (p *LinodeProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { recordsByZoneID := make(map[string][]*linodego.DomainRecord) zones, err := p.fetchZones() diff --git a/provider/linode_test.go b/provider/linode_test.go index 5b7169f05..9abf1950c 100644 --- a/provider/linode_test.go +++ b/provider/linode_test.go @@ -353,7 +353,7 @@ func TestLinodeApplyChanges(t *testing.T) { }, ).Return(&linodego.DomainRecord{}, nil).Once() - err := provider.ApplyChanges(&plan.Changes{ + err := provider.ApplyChanges(context.Background(), &plan.Changes{ Create: []*endpoint.Endpoint{{ DNSName: "create.bar.io", RecordType: "A", @@ -428,7 +428,7 @@ func TestLinodeApplyChangesTargetAdded(t *testing.T) { }, ).Return(&linodego.DomainRecord{}, nil).Once() - err := provider.ApplyChanges(&plan.Changes{ + err := provider.ApplyChanges(context.Background(), &plan.Changes{ // From 1 target to 2 UpdateNew: []*endpoint.Endpoint{{ DNSName: "example.com", @@ -484,7 +484,7 @@ func TestLinodeApplyChangesTargetRemoved(t *testing.T) { 11, ).Return(nil).Once() - err := provider.ApplyChanges(&plan.Changes{ + err := provider.ApplyChanges(context.Background(), &plan.Changes{ // From 2 targets to 1 UpdateNew: []*endpoint.Endpoint{{ DNSName: "example.com", @@ -521,7 +521,7 @@ func TestLinodeApplyChangesNoChanges(t *testing.T) { mock.Anything, ).Return([]*linodego.DomainRecord{{ID: 11, Name: "", Type: "A", Target: "targetA"}}, nil).Once() - err := provider.ApplyChanges(&plan.Changes{}) + err := provider.ApplyChanges(context.Background(), &plan.Changes{}) require.NoError(t, err) mockDomainClient.AssertExpectations(t) diff --git a/provider/ns1.go b/provider/ns1.go new file mode 100644 index 000000000..988481d38 --- /dev/null +++ b/provider/ns1.go @@ -0,0 +1,319 @@ +/* +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 provider + +import ( + "context" + "crypto/tls" + "fmt" + "net/http" + "os" + "strings" + + log "github.com/sirupsen/logrus" + + api "gopkg.in/ns1/ns1-go.v2/rest" + "gopkg.in/ns1/ns1-go.v2/rest/model/dns" + + "github.com/kubernetes-incubator/external-dns/endpoint" + "github.com/kubernetes-incubator/external-dns/plan" +) + +const ( + // ns1Create is a ChangeAction enum value + ns1Create = "CREATE" + // ns1Delete is a ChangeAction enum value + ns1Delete = "DELETE" + // ns1Update is a ChangeAction enum value + ns1Update = "UPDATE" + // ns1DefaultTTL is the default ttl for ttls that are not set + ns1DefaultTTL = 10 +) + +// NS1DomainClient is a subset of the NS1 API the the provider uses, to ease testing +type NS1DomainClient interface { + CreateRecord(r *dns.Record) (*http.Response, error) + DeleteRecord(zone string, domain string, t string) (*http.Response, error) + UpdateRecord(r *dns.Record) (*http.Response, error) + GetZone(zone string) (*dns.Zone, *http.Response, error) + ListZones() ([]*dns.Zone, *http.Response, error) +} + +// NS1DomainService wraps the API and fulfills the NS1DomainClient interface +type NS1DomainService struct { + service *api.Client +} + +// CreateRecord wraps the Create method of the API's Record service +func (n NS1DomainService) CreateRecord(r *dns.Record) (*http.Response, error) { + return n.service.Records.Create(r) +} + +// DeleteRecord wraps the Delete method of the API's Record service +func (n NS1DomainService) DeleteRecord(zone string, domain string, t string) (*http.Response, error) { + return n.service.Records.Delete(zone, domain, t) +} + +// UpdateRecord wraps the Update method of the API's Record service +func (n NS1DomainService) UpdateRecord(r *dns.Record) (*http.Response, error) { + return n.service.Records.Update(r) +} + +// GetZone wraps the Get method of the API's Zones service +func (n NS1DomainService) GetZone(zone string) (*dns.Zone, *http.Response, error) { + return n.service.Zones.Get(zone) +} + +// ListZones wraps the List method of the API's Zones service +func (n NS1DomainService) ListZones() ([]*dns.Zone, *http.Response, error) { + return n.service.Zones.List() +} + +// NS1Config passes cli args to the NS1Provider +type NS1Config struct { + DomainFilter DomainFilter + ZoneIDFilter ZoneIDFilter + NS1Endpoint string + NS1IgnoreSSL bool + DryRun bool +} + +// NS1Provider is the NS1 provider +type NS1Provider struct { + client NS1DomainClient + domainFilter DomainFilter + zoneIDFilter ZoneIDFilter + dryRun bool +} + +// NewNS1Provider creates a new NS1 Provider +func NewNS1Provider(config NS1Config) (*NS1Provider, error) { + return newNS1ProviderWithHTTPClient(config, http.DefaultClient) +} + +func newNS1ProviderWithHTTPClient(config NS1Config, client *http.Client) (*NS1Provider, error) { + token, ok := os.LookupEnv("NS1_APIKEY") + if !ok { + return nil, fmt.Errorf("NS1_APIKEY environment variable is not set") + } + clientArgs := []func(*api.Client){api.SetAPIKey(token)} + if config.NS1Endpoint != "" { + log.Infof("ns1-endpoint flag is set, targeting endpoint at %s", config.NS1Endpoint) + clientArgs = append(clientArgs, api.SetEndpoint(config.NS1Endpoint)) + } + + if config.NS1IgnoreSSL == true { + log.Info("ns1-ignoressl flag is True, skipping SSL verification") + defaultTransport := http.DefaultTransport.(*http.Transport) + tr := &http.Transport{ + Proxy: defaultTransport.Proxy, + DialContext: defaultTransport.DialContext, + MaxIdleConns: defaultTransport.MaxIdleConns, + IdleConnTimeout: defaultTransport.IdleConnTimeout, + ExpectContinueTimeout: defaultTransport.ExpectContinueTimeout, + TLSHandshakeTimeout: defaultTransport.TLSHandshakeTimeout, + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + client.Transport = tr + } + + apiClient := api.NewClient(client, clientArgs...) + + provider := &NS1Provider{ + client: NS1DomainService{apiClient}, + domainFilter: config.DomainFilter, + zoneIDFilter: config.ZoneIDFilter, + } + return provider, nil +} + +// Records returns the endpoints this provider knows about +func (p *NS1Provider) Records() ([]*endpoint.Endpoint, error) { + zones, err := p.zonesFiltered() + if err != nil { + return nil, err + } + + var endpoints []*endpoint.Endpoint + + for _, zone := range zones { + + // TODO handle Header Codes + zoneData, _, err := p.client.GetZone(zone.String()) + if err != nil { + return nil, err + } + + for _, record := range zoneData.Records { + if supportedRecordType(record.Type) { + endpoints = append(endpoints, endpoint.NewEndpointWithTTL( + record.Domain, + record.Type, + endpoint.TTL(record.TTL), + record.ShortAns..., + ), + ) + } + } + } + + return endpoints, nil +} + +// ns1BuildRecord returns a dns.Record for a change set +func ns1BuildRecord(zoneName string, change *ns1Change) *dns.Record { + record := dns.NewRecord(zoneName, change.Endpoint.DNSName, change.Endpoint.RecordType) + for _, v := range change.Endpoint.Targets { + record.AddAnswer(dns.NewAnswer(strings.Split(v, " "))) + } + // set detault ttl + var ttl = ns1DefaultTTL + if change.Endpoint.RecordTTL.IsConfigured() { + ttl = int(change.Endpoint.RecordTTL) + } + record.TTL = ttl + + return record +} + +// ns1SubmitChanges takes an array of changes and sends them to NS1 +func (p *NS1Provider) ns1SubmitChanges(changes []*ns1Change) error { + // return early if there is nothing to change + if len(changes) == 0 { + return nil + } + + zones, err := p.zonesFiltered() + if err != nil { + return err + } + + // separate into per-zone change sets to be passed to the API. + changesByZone := ns1ChangesByZone(zones, changes) + for zoneName, changes := range changesByZone { + for _, change := range changes { + record := ns1BuildRecord(zoneName, change) + logFields := log.Fields{ + "record": record.Domain, + "type": record.Type, + "ttl": record.TTL, + "action": change.Action, + "zone": zoneName, + } + + log.WithFields(logFields).Info("Changing record.") + + if p.dryRun { + continue + } + + switch change.Action { + case ns1Create: + _, err := p.client.CreateRecord(record) + if err != nil { + return err + } + case ns1Delete: + _, err := p.client.DeleteRecord(zoneName, record.Domain, record.Type) + if err != nil { + return err + } + case ns1Update: + _, err := p.client.UpdateRecord(record) + if err != nil { + return err + } + } + } + } + return nil +} + +// Zones returns the list of hosted zones. +func (p *NS1Provider) zonesFiltered() ([]*dns.Zone, error) { + // TODO handle Header Codes + zones, _, err := p.client.ListZones() + if err != nil { + return nil, err + } + + toReturn := []*dns.Zone{} + + for _, z := range zones { + if p.domainFilter.Match(z.Zone) && p.zoneIDFilter.Match(z.ID) { + toReturn = append(toReturn, z) + log.Debugf("Matched %s", z.Zone) + } else { + log.Debugf("Filtered %s", z.Zone) + } + } + + return toReturn, nil +} + +// ns1Change differentiates between ChangeActions +type ns1Change struct { + Action string + Endpoint *endpoint.Endpoint +} + +// ApplyChanges applies a given set of changes in a given zone. +func (p *NS1Provider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { + combinedChanges := make([]*ns1Change, 0, len(changes.Create)+len(changes.UpdateNew)+len(changes.Delete)) + + combinedChanges = append(combinedChanges, newNS1Changes(ns1Create, changes.Create)...) + combinedChanges = append(combinedChanges, newNS1Changes(ns1Update, changes.UpdateNew)...) + combinedChanges = append(combinedChanges, newNS1Changes(ns1Delete, changes.Delete)...) + + return p.ns1SubmitChanges(combinedChanges) +} + +// newNS1Changes returns a collection of Changes based on the given records and action. +func newNS1Changes(action string, endpoints []*endpoint.Endpoint) []*ns1Change { + changes := make([]*ns1Change, 0, len(endpoints)) + + for _, endpoint := range endpoints { + changes = append(changes, &ns1Change{ + Action: action, + Endpoint: endpoint, + }, + ) + } + + return changes +} + +// ns1ChangesByZone separates a multi-zone change into a single change per zone. +func ns1ChangesByZone(zones []*dns.Zone, changeSets []*ns1Change) map[string][]*ns1Change { + changes := make(map[string][]*ns1Change) + zoneNameIDMapper := zoneIDName{} + for _, z := range zones { + zoneNameIDMapper.Add(z.Zone, z.Zone) + changes[z.Zone] = []*ns1Change{} + } + + for _, c := range changeSets { + zone, _ := zoneNameIDMapper.FindZone(c.Endpoint.DNSName) + if zone == "" { + log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected ", c.Endpoint.DNSName) + continue + } + changes[zone] = append(changes[zone], c) + } + + return changes +} diff --git a/provider/ns1_test.go b/provider/ns1_test.go new file mode 100644 index 000000000..6fc5ea823 --- /dev/null +++ b/provider/ns1_test.go @@ -0,0 +1,307 @@ +/* +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 provider + +import ( + "context" + "fmt" + "net/http" + "os" + "testing" + + "github.com/kubernetes-incubator/external-dns/endpoint" + "github.com/kubernetes-incubator/external-dns/plan" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + api "gopkg.in/ns1/ns1-go.v2/rest" + "gopkg.in/ns1/ns1-go.v2/rest/model/dns" +) + +type MockNS1DomainClient struct { + mock.Mock +} + +func (m *MockNS1DomainClient) CreateRecord(r *dns.Record) (*http.Response, error) { + return nil, nil +} + +func (m *MockNS1DomainClient) DeleteRecord(zone string, domain string, t string) (*http.Response, error) { + return nil, nil +} + +func (m *MockNS1DomainClient) UpdateRecord(r *dns.Record) (*http.Response, error) { + return nil, nil +} + +func (m *MockNS1DomainClient) GetZone(zone string) (*dns.Zone, *http.Response, error) { + r := &dns.ZoneRecord{ + Domain: "test.foo.com", + ShortAns: []string{"2.2.2.2"}, + TTL: 3600, + Type: "A", + ID: "123456789abcdefghijklmno", + } + z := &dns.Zone{ + Zone: "foo.com", + Records: []*dns.ZoneRecord{r}, + TTL: 3600, + ID: "12345678910111213141516a", + } + + if zone == "foo.com" { + return z, nil, nil + } + return nil, nil, nil +} + +func (m *MockNS1DomainClient) ListZones() ([]*dns.Zone, *http.Response, error) { + zones := []*dns.Zone{ + {Zone: "foo.com", ID: "12345678910111213141516a"}, + {Zone: "bar.com", ID: "12345678910111213141516b"}, + } + return zones, nil, nil +} + +type MockNS1GetZoneFail struct{} + +func (m *MockNS1GetZoneFail) CreateRecord(r *dns.Record) (*http.Response, error) { + return nil, nil +} + +func (m *MockNS1GetZoneFail) DeleteRecord(zone string, domain string, t string) (*http.Response, error) { + return nil, nil +} + +func (m *MockNS1GetZoneFail) UpdateRecord(r *dns.Record) (*http.Response, error) { + return nil, nil +} + +func (m *MockNS1GetZoneFail) GetZone(zone string) (*dns.Zone, *http.Response, error) { + return nil, nil, api.ErrZoneMissing +} + +func (m *MockNS1GetZoneFail) ListZones() ([]*dns.Zone, *http.Response, error) { + zones := []*dns.Zone{ + {Zone: "foo.com", ID: "12345678910111213141516a"}, + {Zone: "bar.com", ID: "12345678910111213141516b"}, + } + return zones, nil, nil +} + +type MockNS1ListZonesFail struct{} + +func (m *MockNS1ListZonesFail) CreateRecord(r *dns.Record) (*http.Response, error) { + return nil, nil +} + +func (m *MockNS1ListZonesFail) DeleteRecord(zone string, domain string, t string) (*http.Response, error) { + return nil, nil +} + +func (m *MockNS1ListZonesFail) UpdateRecord(r *dns.Record) (*http.Response, error) { + return nil, nil +} + +func (m *MockNS1ListZonesFail) GetZone(zone string) (*dns.Zone, *http.Response, error) { + return &dns.Zone{}, nil, nil +} + +func (m *MockNS1ListZonesFail) ListZones() ([]*dns.Zone, *http.Response, error) { + return nil, nil, fmt.Errorf("no zones available") +} + +func TestNS1Records(t *testing.T) { + provider := &NS1Provider{ + client: &MockNS1DomainClient{}, + domainFilter: NewDomainFilter([]string{"foo.com."}), + zoneIDFilter: NewZoneIDFilter([]string{""}), + } + records, err := provider.Records() + require.NoError(t, err) + assert.Equal(t, 1, len(records)) + + provider.client = &MockNS1GetZoneFail{} + _, err = provider.Records() + require.Error(t, err) + + provider.client = &MockNS1ListZonesFail{} + _, err = provider.Records() + require.Error(t, err) +} + +func TestNewNS1Provider(t *testing.T) { + _ = os.Setenv("NS1_APIKEY", "xxxxxxxxxxxxxxxxx") + testNS1Config := NS1Config{ + DomainFilter: NewDomainFilter([]string{"foo.com."}), + ZoneIDFilter: NewZoneIDFilter([]string{""}), + DryRun: false, + } + _, err := NewNS1Provider(testNS1Config) + require.NoError(t, err) + + _ = os.Unsetenv("NS1_APIKEY") + _, err = NewNS1Provider(testNS1Config) + require.Error(t, err) +} + +func TestNS1Zones(t *testing.T) { + provider := &NS1Provider{ + client: &MockNS1DomainClient{}, + domainFilter: NewDomainFilter([]string{"foo.com."}), + zoneIDFilter: NewZoneIDFilter([]string{""}), + } + + zones, err := provider.zonesFiltered() + require.NoError(t, err) + + validateNS1Zones(t, zones, []*dns.Zone{ + {Zone: "foo.com"}, + }) +} + +func validateNS1Zones(t *testing.T, zones []*dns.Zone, expected []*dns.Zone) { + require.Len(t, zones, len(expected)) + + for i, zone := range zones { + assert.Equal(t, expected[i].Zone, zone.Zone) + } +} + +func TestNS1BuildRecord(t *testing.T) { + change := &ns1Change{ + Action: ns1Create, + Endpoint: &endpoint.Endpoint{ + DNSName: "new", + Targets: endpoint.Targets{"target"}, + RecordType: "A", + }, + } + record := ns1BuildRecord("foo.com", change) + assert.Equal(t, "foo.com", record.Zone) + assert.Equal(t, "new.foo.com", record.Domain) + assert.Equal(t, ns1DefaultTTL, record.TTL) + + changeWithTTL := &ns1Change{ + Action: ns1Create, + Endpoint: &endpoint.Endpoint{ + DNSName: "new-b", + Targets: endpoint.Targets{"target"}, + RecordType: "A", + RecordTTL: 100, + }, + } + record = ns1BuildRecord("foo.com", changeWithTTL) + assert.Equal(t, "foo.com", record.Zone) + assert.Equal(t, "new-b.foo.com", record.Domain) + assert.Equal(t, 100, record.TTL) +} + +func TestNS1ApplyChanges(t *testing.T) { + changes := &plan.Changes{} + provider := &NS1Provider{ + client: &MockNS1DomainClient{}, + } + changes.Create = []*endpoint.Endpoint{ + {DNSName: "new.foo.com", Targets: endpoint.Targets{"target"}}, + {DNSName: "new.subdomain.bar.com", Targets: endpoint.Targets{"target"}}, + } + changes.Delete = []*endpoint.Endpoint{{DNSName: "test.foo.com", Targets: endpoint.Targets{"target"}}} + changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "test.foo.com", Targets: endpoint.Targets{"target-new"}}} + err := provider.ApplyChanges(context.Background(), changes) + require.NoError(t, err) + + // empty changes + changes.Create = []*endpoint.Endpoint{} + changes.Delete = []*endpoint.Endpoint{} + changes.UpdateNew = []*endpoint.Endpoint{} + err = provider.ApplyChanges(context.Background(), changes) + require.NoError(t, err) +} + +func TestNewNS1Changes(t *testing.T) { + endpoints := []*endpoint.Endpoint{ + { + DNSName: "testa.foo.com", + Targets: endpoint.Targets{"target-old"}, + RecordType: "A", + }, + { + DNSName: "testba.bar.com", + Targets: endpoint.Targets{"target-new"}, + RecordType: "A", + }, + } + expected := []*ns1Change{ + { + Action: "ns1Create", + Endpoint: endpoints[0], + }, + { + Action: "ns1Create", + Endpoint: endpoints[1], + }, + } + changes := newNS1Changes("ns1Create", endpoints) + require.Len(t, changes, len(expected)) + assert.Equal(t, expected, changes) +} + +func TestNewNS1ChangesByZone(t *testing.T) { + provider := &NS1Provider{ + client: &MockNS1DomainClient{}, + } + zones, _ := provider.zonesFiltered() + changeSets := []*ns1Change{ + { + Action: "ns1Create", + Endpoint: &endpoint.Endpoint{ + DNSName: "new.foo.com", + Targets: endpoint.Targets{"target"}, + RecordType: "A", + }, + }, + { + Action: "ns1Create", + Endpoint: &endpoint.Endpoint{ + DNSName: "unrelated.bar.com", + Targets: endpoint.Targets{"target"}, + RecordType: "A", + }, + }, + { + Action: "ns1Delete", + Endpoint: &endpoint.Endpoint{ + DNSName: "test.foo.com", + Targets: endpoint.Targets{"target"}, + RecordType: "A", + }, + }, + { + Action: "ns1Update", + Endpoint: &endpoint.Endpoint{ + DNSName: "test.foo.com", + Targets: endpoint.Targets{"target-new"}, + RecordType: "A", + }, + }, + } + + changes := ns1ChangesByZone(zones, changeSets) + assert.Len(t, changes["bar.com"], 1) + assert.Len(t, changes["foo.com"], 3) +} diff --git a/provider/oci.go b/provider/oci.go index aa0c03411..cb4f6c23b 100644 --- a/provider/oci.go +++ b/provider/oci.go @@ -201,7 +201,7 @@ func (p *OCIProvider) Records() ([]*endpoint.Endpoint, error) { } // ApplyChanges applies a given set of changes to a given zone. -func (p *OCIProvider) ApplyChanges(changes *plan.Changes) error { +func (p *OCIProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { log.Debugf("Processing chages: %+v", changes) ops := []dns.RecordOperation{} @@ -217,7 +217,6 @@ func (p *OCIProvider) ApplyChanges(changes *plan.Changes) error { return nil } - ctx := context.Background() zones, err := p.zones(ctx) if err != nil { return errors.Wrap(err, "fetching zones") diff --git a/provider/oci_test.go b/provider/oci_test.go index 89056812c..a4a476d96 100644 --- a/provider/oci_test.go +++ b/provider/oci_test.go @@ -829,7 +829,7 @@ func TestOCIApplyChanges(t *testing.T) { NewZoneIDFilter([]string{""}), tc.dryRun, ) - err := provider.ApplyChanges(tc.changes) + err := provider.ApplyChanges(context.Background(), tc.changes) require.Equal(t, tc.err, err) endpoints, err := provider.Records() require.NoError(t, err) diff --git a/provider/pdns.go b/provider/pdns.go index 8bf888daf..4622971f9 100644 --- a/provider/pdns.go +++ b/provider/pdns.go @@ -443,7 +443,7 @@ func (p *PDNSProvider) Records() (endpoints []*endpoint.Endpoint, _ error) { // ApplyChanges takes a list of changes (endpoints) and updates the PDNS server // by sending the correct HTTP PATCH requests to a matching zone -func (p *PDNSProvider) ApplyChanges(changes *plan.Changes) error { +func (p *PDNSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { startTime := time.Now() diff --git a/provider/provider.go b/provider/provider.go index 23b54e1e2..7c6f1a61e 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -17,6 +17,7 @@ limitations under the License. package provider import ( + "context" "net" "strings" @@ -27,9 +28,20 @@ import ( // Provider defines the interface DNS providers should implement. type Provider interface { Records() ([]*endpoint.Endpoint, error) - ApplyChanges(changes *plan.Changes) error + ApplyChanges(ctx context.Context, changes *plan.Changes) error } +type contextKey struct { + name string +} + +func (k *contextKey) String() string { return "provider context value " + k.name } + +// RecordsContextKey is a context key. It can be used during ApplyChanges +// to access previously cached records. The associated value will be of +// type []*endpoint.Endpoint. +var RecordsContextKey = &contextKey{"records"} + // ensureTrailingDot ensures that the hostname receives a trailing dot if it hasn't already. func ensureTrailingDot(hostname string) string { if net.ParseIP(hostname) != nil { diff --git a/provider/rcode0.go b/provider/rcode0.go index 866dd735c..d9a0adcdb 100644 --- a/provider/rcode0.go +++ b/provider/rcode0.go @@ -17,6 +17,7 @@ limitations under the License. package provider import ( + "context" "fmt" "net/url" "os" @@ -141,7 +142,7 @@ func (p *RcodeZeroProvider) Records() ([]*endpoint.Endpoint, error) { } // ApplyChanges applies a given set of changes in a given zone. -func (p *RcodeZeroProvider) ApplyChanges(changes *plan.Changes) error { +func (p *RcodeZeroProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { combinedChanges := make([]*rc0.RRSetChange, 0, len(changes.Create)+len(changes.UpdateNew)+len(changes.Delete)) diff --git a/provider/rcode0_test.go b/provider/rcode0_test.go index 9355a3733..904d2a8e6 100644 --- a/provider/rcode0_test.go +++ b/provider/rcode0_test.go @@ -17,6 +17,7 @@ limitations under the License. package provider import ( + "context" "fmt" "os" "testing" @@ -102,7 +103,7 @@ func TestRcodeZeroProvider_ApplyChanges(t *testing.T) { changes := mockChanges() - err := provider.ApplyChanges(changes) + err := provider.ApplyChanges(context.Background(), changes) if err != nil { t.Errorf("should not fail, %s", err) diff --git a/provider/rfc2136.go b/provider/rfc2136.go index 0943864e2..b2d1d3515 100644 --- a/provider/rfc2136.go +++ b/provider/rfc2136.go @@ -17,6 +17,7 @@ limitations under the License. package provider import ( + "context" "fmt" "net" "strconv" @@ -195,7 +196,7 @@ func (r rfc2136Provider) List() ([]dns.RR, error) { } // ApplyChanges applies a given set of changes in a given zone. -func (r rfc2136Provider) ApplyChanges(changes *plan.Changes) error { +func (r rfc2136Provider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { log.Debugf("ApplyChanges") for _, ep := range changes.Create { @@ -242,7 +243,7 @@ func (r rfc2136Provider) AddRecord(ep *endpoint.Endpoint) error { log.Debugf("AddRecord.ep=%s", ep) for _, target := range ep.Targets { newRR := fmt.Sprintf("%s %d %s %s", ep.DNSName, ep.RecordTTL, ep.RecordType, target) - log.Debugf("Adding RR: %s", newRR) + log.Infof("Adding RR: %s", newRR) rr, err := dns.NewRR(newRR) if err != nil { @@ -269,7 +270,7 @@ func (r rfc2136Provider) RemoveRecord(ep *endpoint.Endpoint) error { log.Debugf("RemoveRecord.ep=%s", ep) newRR := fmt.Sprintf("%s 0 %s 0.0.0.0", ep.DNSName, ep.RecordType) - log.Debugf("Removing RR: %s", newRR) + log.Infof("Removing RR: %s", newRR) rr, err := dns.NewRR(newRR) if err != nil { diff --git a/provider/rfc2136_test.go b/provider/rfc2136_test.go index cb9ef8a2b..061fed497 100644 --- a/provider/rfc2136_test.go +++ b/provider/rfc2136_test.go @@ -17,6 +17,7 @@ limitations under the License. package provider import ( + "context" "strings" "testing" @@ -149,7 +150,7 @@ func TestRfc2136ApplyChanges(t *testing.T) { }, } - err = provider.ApplyChanges(p) + err = provider.ApplyChanges(context.Background(), p) assert.NoError(t, err) assert.Equal(t, 2, len(stub.createMsgs)) diff --git a/provider/transip.go b/provider/transip.go new file mode 100644 index 000000000..1fff1daa2 --- /dev/null +++ b/provider/transip.go @@ -0,0 +1,374 @@ +package provider + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/kubernetes-incubator/external-dns/endpoint" + "github.com/kubernetes-incubator/external-dns/plan" + log "github.com/sirupsen/logrus" + "github.com/transip/gotransip" + transip "github.com/transip/gotransip/domain" +) + +const ( + // 60 seconds is the current minimal TTL for TransIP and will replace unconfigured + // TTL's for Endpoints + transipMinimalValidTTL = 60 +) + +// TransIPProvider is an implementation of Provider for TransIP. +type TransIPProvider struct { + client gotransip.SOAPClient + domainFilter DomainFilter + dryRun bool +} + +// NewTransIPProvider initializes a new TransIP Provider. +func NewTransIPProvider(accountName, privateKeyFile string, domainFilter DomainFilter, dryRun bool) (*TransIPProvider, error) { + // check given arguments + if accountName == "" { + return nil, errors.New("required --transip-account not set") + } + + if privateKeyFile == "" { + return nil, errors.New("required --transip-keyfile not set") + } + + var apiMode gotransip.APIMode + if dryRun { + apiMode = gotransip.APIModeReadOnly + } else { + apiMode = gotransip.APIModeReadWrite + } + + // create new TransIP API client + c, err := gotransip.NewSOAPClient(gotransip.ClientConfig{ + AccountName: accountName, + PrivateKeyPath: privateKeyFile, + Mode: apiMode, + }) + if err != nil { + return nil, fmt.Errorf("could not setup TransIP API client: %s", err.Error()) + } + + // return tipCloud struct + return &TransIPProvider{ + client: c, + domainFilter: domainFilter, + dryRun: dryRun, + }, nil +} + +// ApplyChanges applies a given set of changes in a given zone. +func (p *TransIPProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { + // build zonefinder with all our zones so we can use FindZone + // and a mapping of zones and their domain name + zones, err := p.fetchZones() + if err != nil { + return err + } + + zoneNameMapper := zoneIDName{} + zonesByName := make(map[string]transip.Domain) + updatedZones := make(map[string]bool) + for _, zone := range zones { + // TransIP API doesn't expose a unique identifier for zones, other than than + // the domain name itself + zoneNameMapper.Add(zone.Name, zone.Name) + zonesByName[zone.Name] = zone + } + + // first see if we need to delete anything + for _, ep := range changes.Delete { + log.WithFields(log.Fields{"record": ep.DNSName, "type": ep.RecordType}).Info("endpoint has to go") + + zone, err := p.zoneForZoneName(ep.DNSName, zoneNameMapper, zonesByName) + if err != nil { + log.Errorf("could not find zone for %s: %s", ep.DNSName, err.Error()) + continue + } + + log.Debugf("removing records for %s", zone.Name) + + // remove current records from DNS entry set + entries := p.removeEndpointFromEntries(ep, zone) + + // update zone in zone map + zone.DNSEntries = entries + zonesByName[zone.Name] = zone + // flag zone for updating + updatedZones[zone.Name] = true + } + + for _, ep := range changes.Create { + log.WithFields(log.Fields{"record": ep.DNSName, "type": ep.RecordType}).Info("endpoint is missing") + + zone, err := p.zoneForZoneName(ep.DNSName, zoneNameMapper, zonesByName) + if err != nil { + log.Errorf("could not find zone for %s: %s", ep.DNSName, err.Error()) + continue + } + + log.Debugf("creating records for %s", zone.Name) + + // add new entries to set + zone.DNSEntries = p.addEndpointToEntries(ep, zone, zone.DNSEntries) + + // update zone in zone map + zonesByName[zone.Name] = zone + // flag zone for updating + updatedZones[zone.Name] = true + log.WithFields(log.Fields{"zone": zone.Name}).Debug("flagging for update") + } + + for _, ep := range changes.UpdateNew { + log.WithFields(log.Fields{"record": ep.DNSName, "type": ep.RecordType}).Debug("needs updating") + + zone, err := p.zoneForZoneName(ep.DNSName, zoneNameMapper, zonesByName) + if err != nil { + log.WithFields(log.Fields{"record": ep.DNSName}).Warn(err.Error()) + continue + } + + // updating the records is basically finding all matching records according + // to the name and the type, removing them from the set and add the new + // records + log.WithFields(log.Fields{ + "zone": zone.Name, + "dnsname": ep.DNSName, + "recordtype": ep.RecordType, + }).Debug("removing matching entries") + + // remove current records from DNS entry set + entries := p.removeEndpointFromEntries(ep, zone) + + // add new entries to set + entries = p.addEndpointToEntries(ep, zone, entries) + + // check to see if actually anything changed in the DNSEntry set + if p.dnsEntriesAreEqual(entries, zone.DNSEntries) { + log.WithFields(log.Fields{"zone": zone.Name}).Debug("not updating identical entries") + continue + } + + // update zone in zone map + zone.DNSEntries = entries + zonesByName[zone.Name] = zone + // flag zone for updating + updatedZones[zone.Name] = true + + log.WithFields(log.Fields{"zone": zone.Name}).Debug("flagging for update") + } + + // go over all updated zones and set new DNSEntry set + for uz := range updatedZones { + zone, ok := zonesByName[uz] + if !ok { + log.WithFields(log.Fields{"zone": uz}).Debug("updated zone no longer found") + continue + } + + if p.dryRun { + log.WithFields(log.Fields{"zone": zone.Name}).Info("not updating in dry-run mode") + continue + } + + log.WithFields(log.Fields{"zone": zone.Name}).Info("updating DNS entries") + if err := transip.SetDNSEntries(p.client, zone.Name, zone.DNSEntries); err != nil { + log.WithFields(log.Fields{"zone": zone.Name, "error": err.Error()}).Warn("failed to update") + } + } + + return nil +} + +// fetchZones returns a list of all domains within the account +func (p *TransIPProvider) fetchZones() ([]transip.Domain, error) { + domainNames, err := transip.GetDomainNames(p.client) + if err != nil { + return nil, err + } + + domains, err := transip.BatchGetInfo(p.client, domainNames) + if err != nil { + return nil, err + } + + var zones []transip.Domain + for _, d := range domains { + if !p.domainFilter.Match(d.Name) { + continue + } + + zones = append(zones, d) + } + + return zones, nil +} + +// Zones returns the list of hosted zones. +func (p *TransIPProvider) Zones() ([]transip.Domain, error) { + zones, err := p.fetchZones() + if err != nil { + return nil, err + } + + return zones, nil +} + +// Records returns the list of records in a given zone. +func (p *TransIPProvider) Records() ([]*endpoint.Endpoint, error) { + zones, err := p.Zones() + if err != nil { + return nil, err + } + + var endpoints []*endpoint.Endpoint + var name string + // go over all zones and their DNS entries and create endpoints for them + for _, zone := range zones { + for _, r := range zone.DNSEntries { + if !supportedRecordType(string(r.Type)) { + continue + } + + name = p.endpointNameForRecord(r, zone) + endpoints = append(endpoints, endpoint.NewEndpointWithTTL(name, string(r.Type), endpoint.TTL(r.TTL), r.Content)) + } + } + + return endpoints, nil +} + +// endpointNameForRecord returns "www.example.org" for DNSEntry with Name "www" and +// Doman with Name "example.org" +func (p *TransIPProvider) endpointNameForRecord(r transip.DNSEntry, d transip.Domain) string { + // root name is identified by "@" and should be translated to domain name for + // the endpoint entry. + if r.Name == "@" { + return d.Name + } + + return fmt.Sprintf("%s.%s", r.Name, d.Name) +} + +// recordNameForEndpoint returns "www" for Endpoint with DNSName "www.example.org" +// and Domain with Name "example.org" +func (p *TransIPProvider) recordNameForEndpoint(ep *endpoint.Endpoint, d transip.Domain) string { + // root name is identified by "@" and should be translated to domain name for + // the endpoint entry. + if ep.DNSName == d.Name { + return "@" + } + + return strings.TrimSuffix(ep.DNSName, "."+d.Name) +} + +// getMinimalValidTTL returns max between given Endpoint's RecordTTL and +// transipMinimalValidTTL +func (p *TransIPProvider) getMinimalValidTTL(ep *endpoint.Endpoint) int64 { + // TTL cannot be lower than transipMinimalValidTTL + if ep.RecordTTL < transipMinimalValidTTL { + return transipMinimalValidTTL + } + + return int64(ep.RecordTTL) +} + +// dnsEntriesAreEqual compares the entries in 2 sets and returns true if the +// content of the entries is equal +func (p *TransIPProvider) dnsEntriesAreEqual(a, b transip.DNSEntries) bool { + if len(a) != len(b) { + return false + } + + match := 0 + for _, aa := range a { + for _, bb := range b { + if aa.Content != bb.Content { + continue + } + + if aa.Name != bb.Name { + continue + } + + if aa.TTL != bb.TTL { + continue + } + + if aa.Type != bb.Type { + continue + } + + match += 1 + } + } + + return (len(a) == match) +} + +// removeEndpointFromEntries removes DNS entries from zone's set that match the +// type and name from given endpoint and returns the resulting DNS entry set +func (p *TransIPProvider) removeEndpointFromEntries(ep *endpoint.Endpoint, zone transip.Domain) transip.DNSEntries { + // create new entry set + entries := transip.DNSEntries{} + // go over each DNS entry to see if it is a match + for _, e := range zone.DNSEntries { + // if we have match, don't copy it to the new entry set + if p.endpointNameForRecord(e, zone) == ep.DNSName && string(e.Type) == ep.RecordType { + log.WithFields(log.Fields{ + "name": e.Name, + "content": e.Content, + "type": e.Type, + }).Debug("found match") + continue + } + + entries = append(entries, e) + } + + return entries +} + +// addEndpointToEntries creates DNS entries for given endpoint and returns +// resulting DNS entry set +func (p *TransIPProvider) addEndpointToEntries(ep *endpoint.Endpoint, zone transip.Domain, entries transip.DNSEntries) transip.DNSEntries { + ttl := p.getMinimalValidTTL(ep) + for _, target := range ep.Targets { + log.WithFields(log.Fields{ + "zone": zone.Name, + "dnsname": ep.DNSName, + "recordtype": ep.RecordType, + "ttl": ttl, + "target": target, + }).Debugf("adding new record") + entries = append(entries, transip.DNSEntry{ + Name: p.recordNameForEndpoint(ep, zone), + TTL: ttl, + Type: transip.DNSEntryType(ep.RecordType), + Content: target, + }) + } + + return entries +} + +// zoneForZoneName returns the zone mapped to given name or error if zone could +// not be found +func (p *TransIPProvider) zoneForZoneName(name string, m zoneIDName, z map[string]transip.Domain) (transip.Domain, error) { + _, zoneName := m.FindZone(name) + if zoneName == "" { + return transip.Domain{}, fmt.Errorf("could not find zoneName for %s", name) + } + + zone, ok := z[zoneName] + if !ok { + return zone, fmt.Errorf("could not find zone for %s", zoneName) + } + + return zone, nil +} diff --git a/provider/transip_test.go b/provider/transip_test.go new file mode 100644 index 000000000..b195a246d --- /dev/null +++ b/provider/transip_test.go @@ -0,0 +1,215 @@ +package provider + +import ( + "testing" + + "github.com/kubernetes-incubator/external-dns/endpoint" + "github.com/stretchr/testify/assert" + transip "github.com/transip/gotransip/domain" +) + +func TestTransIPDnsEntriesAreEqual(t *testing.T) { + p := TransIPProvider{} + // test with equal set + a := transip.DNSEntries{ + transip.DNSEntry{ + Name: "www.example.org", + Type: transip.DNSEntryTypeCNAME, + TTL: 3600, + Content: "www.example.com", + }, + transip.DNSEntry{ + Name: "www.example.com", + Type: transip.DNSEntryTypeA, + TTL: 3600, + Content: "192.168.0.1", + }, + } + + b := transip.DNSEntries{ + transip.DNSEntry{ + Name: "www.example.com", + Type: transip.DNSEntryTypeA, + TTL: 3600, + Content: "192.168.0.1", + }, + transip.DNSEntry{ + Name: "www.example.org", + Type: transip.DNSEntryTypeCNAME, + TTL: 3600, + Content: "www.example.com", + }, + } + + assert.Equal(t, true, p.dnsEntriesAreEqual(a, b)) + + // change type on one of b's records + b[1].Type = transip.DNSEntryTypeNS + assert.Equal(t, false, p.dnsEntriesAreEqual(a, b)) + b[1].Type = transip.DNSEntryTypeCNAME + + // change ttl on one of b's records + b[1].TTL = 1800 + assert.Equal(t, false, p.dnsEntriesAreEqual(a, b)) + b[1].TTL = 3600 + + // change name on one of b's records + b[1].Name = "example.org" + assert.Equal(t, false, p.dnsEntriesAreEqual(a, b)) + + // remove last entry of b + b = b[:1] + assert.Equal(t, false, p.dnsEntriesAreEqual(a, b)) +} + +func TestTransIPGetMinimalValidTTL(t *testing.T) { + p := TransIPProvider{} + // test with 'unconfigured' TTL + ep := &endpoint.Endpoint{} + assert.Equal(t, int64(transipMinimalValidTTL), p.getMinimalValidTTL(ep)) + + // test with lower than minimal ttl + ep.RecordTTL = (transipMinimalValidTTL - 1) + assert.Equal(t, int64(transipMinimalValidTTL), p.getMinimalValidTTL(ep)) + + // test with higher than minimal ttl + ep.RecordTTL = (transipMinimalValidTTL + 1) + assert.Equal(t, int64(transipMinimalValidTTL+1), p.getMinimalValidTTL(ep)) +} + +func TestTransIPRecordNameForEndpoint(t *testing.T) { + p := TransIPProvider{} + ep := &endpoint.Endpoint{ + DNSName: "example.org", + } + d := transip.Domain{ + Name: "example.org", + } + + assert.Equal(t, "@", p.recordNameForEndpoint(ep, d)) + + ep.DNSName = "www.example.org" + assert.Equal(t, "www", p.recordNameForEndpoint(ep, d)) +} + +func TestTransIPEndpointNameForRecord(t *testing.T) { + p := TransIPProvider{} + r := transip.DNSEntry{ + Name: "@", + } + d := transip.Domain{ + Name: "example.org", + } + + assert.Equal(t, d.Name, p.endpointNameForRecord(r, d)) + + r.Name = "www" + assert.Equal(t, "www.example.org", p.endpointNameForRecord(r, d)) +} + +func TestTransIPAddEndpointToEntries(t *testing.T) { + p := TransIPProvider{} + + // prepare endpoint + ep := &endpoint.Endpoint{ + DNSName: "www.example.org", + RecordType: "A", + RecordTTL: 1800, + Targets: []string{ + "192.168.0.1", + "192.168.0.2", + }, + } + + // prepare zone with DNS entry set + zone := transip.Domain{ + Name: "example.org", + // 2 matching A records + DNSEntries: transip.DNSEntries{ + // 1 non-matching A record + transip.DNSEntry{ + Name: "mail", + Type: transip.DNSEntryTypeA, + Content: "192.168.0.1", + TTL: 3600, + }, + // 1 non-matching MX record + transip.DNSEntry{ + Name: "@", + Type: transip.DNSEntryTypeMX, + Content: "mail.example.org", + TTL: 3600, + }, + }, + } + + // add endpoint to zone's entries + result := p.addEndpointToEntries(ep, zone, zone.DNSEntries) + + assert.Equal(t, 4, len(result)) + assert.Equal(t, "mail", result[0].Name) + assert.Equal(t, transip.DNSEntryTypeA, result[0].Type) + assert.Equal(t, "@", result[1].Name) + assert.Equal(t, transip.DNSEntryTypeMX, result[1].Type) + assert.Equal(t, "www", result[2].Name) + assert.Equal(t, transip.DNSEntryTypeA, result[2].Type) + assert.Equal(t, "192.168.0.1", result[2].Content) + assert.Equal(t, int64(1800), result[2].TTL) + assert.Equal(t, "www", result[3].Name) + assert.Equal(t, transip.DNSEntryTypeA, result[3].Type) + assert.Equal(t, "192.168.0.2", result[3].Content) + assert.Equal(t, int64(1800), result[3].TTL) +} + +func TestTransIPRemoveEndpointFromEntries(t *testing.T) { + p := TransIPProvider{} + + // prepare endpoint + ep := &endpoint.Endpoint{ + DNSName: "www.example.org", + RecordType: "A", + } + + // prepare zone with DNS entry set + zone := transip.Domain{ + Name: "example.org", + // 2 matching A records + DNSEntries: transip.DNSEntries{ + transip.DNSEntry{ + Name: "www", + Type: transip.DNSEntryTypeA, + Content: "192.168.0.1", + TTL: 3600, + }, + transip.DNSEntry{ + Name: "www", + Type: transip.DNSEntryTypeA, + Content: "192.168.0.2", + TTL: 3600, + }, + // 1 non-matching A record + transip.DNSEntry{ + Name: "mail", + Type: transip.DNSEntryTypeA, + Content: "192.168.0.1", + TTL: 3600, + }, + // 1 non-matching MX record + transip.DNSEntry{ + Name: "@", + Type: transip.DNSEntryTypeMX, + Content: "mail.example.org", + TTL: 3600, + }, + }, + } + + // remove endpoint from zone's entries + result := p.removeEndpointFromEntries(ep, zone) + + assert.Equal(t, 2, len(result)) + assert.Equal(t, "mail", result[0].Name) + assert.Equal(t, transip.DNSEntryTypeA, result[0].Type) + assert.Equal(t, "@", result[1].Name) + assert.Equal(t, transip.DNSEntryTypeMX, result[1].Type) +} diff --git a/registry/aws_sd_registry.go b/registry/aws_sd_registry.go index 52c4b4271..64cd95b34 100644 --- a/registry/aws_sd_registry.go +++ b/registry/aws_sd_registry.go @@ -17,6 +17,7 @@ limitations under the License. package registry import ( + "context" "errors" "github.com/kubernetes-incubator/external-dns/endpoint" @@ -64,7 +65,7 @@ func (sdr *AWSSDRegistry) Records() ([]*endpoint.Endpoint, error) { // ApplyChanges filters out records not owned the External-DNS, additionally it adds the required label // inserted in the AWS SD instance as a CreateID field -func (sdr *AWSSDRegistry) ApplyChanges(changes *plan.Changes) error { +func (sdr *AWSSDRegistry) ApplyChanges(ctx context.Context, changes *plan.Changes) error { filteredChanges := &plan.Changes{ Create: changes.Create, UpdateNew: filterOwnedRecords(sdr.ownerID, changes.UpdateNew), @@ -77,7 +78,7 @@ func (sdr *AWSSDRegistry) ApplyChanges(changes *plan.Changes) error { sdr.updateLabels(filteredChanges.UpdateOld) sdr.updateLabels(filteredChanges.Delete) - return sdr.provider.ApplyChanges(filteredChanges) + return sdr.provider.ApplyChanges(ctx, filteredChanges) } func (sdr *AWSSDRegistry) updateLabels(endpoints []*endpoint.Endpoint) { diff --git a/registry/aws_sd_registry_test.go b/registry/aws_sd_registry_test.go index 938fec5c6..6ebda2e01 100644 --- a/registry/aws_sd_registry_test.go +++ b/registry/aws_sd_registry_test.go @@ -17,6 +17,7 @@ limitations under the License. package registry import ( + "context" "testing" "github.com/kubernetes-incubator/external-dns/endpoint" @@ -35,7 +36,7 @@ func (p *inMemoryProvider) Records() ([]*endpoint.Endpoint, error) { return p.endpoints, nil } -func (p *inMemoryProvider) ApplyChanges(changes *plan.Changes) error { +func (p *inMemoryProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { p.onApplyChanges(changes) return nil } @@ -151,7 +152,7 @@ func TestAWSSDRegistry_Records_ApplyChanges(t *testing.T) { r, err := NewAWSSDRegistry(p, "owner") require.NoError(t, err) - err = r.ApplyChanges(changes) + err = r.ApplyChanges(context.Background(), changes) require.NoError(t, err) } diff --git a/registry/noop.go b/registry/noop.go index aadc801a5..701f01c4e 100644 --- a/registry/noop.go +++ b/registry/noop.go @@ -17,6 +17,8 @@ limitations under the License. package registry import ( + "context" + "github.com/kubernetes-incubator/external-dns/endpoint" "github.com/kubernetes-incubator/external-dns/plan" "github.com/kubernetes-incubator/external-dns/provider" @@ -40,6 +42,6 @@ func (im *NoopRegistry) Records() ([]*endpoint.Endpoint, error) { } // ApplyChanges propagates changes to the dns provider -func (im *NoopRegistry) ApplyChanges(changes *plan.Changes) error { - return im.provider.ApplyChanges(changes) +func (im *NoopRegistry) ApplyChanges(ctx context.Context, changes *plan.Changes) error { + return im.provider.ApplyChanges(ctx, changes) } diff --git a/registry/noop_test.go b/registry/noop_test.go index d728fad65..c1688503c 100644 --- a/registry/noop_test.go +++ b/registry/noop_test.go @@ -17,6 +17,7 @@ limitations under the License. package registry import ( + "context" "testing" "github.com/kubernetes-incubator/external-dns/endpoint" @@ -53,7 +54,7 @@ func testNoopRecords(t *testing.T) { RecordType: endpoint.RecordTypeCNAME, }, } - p.ApplyChanges(&plan.Changes{ + p.ApplyChanges(context.Background(), &plan.Changes{ Create: providerRecords, }) @@ -88,13 +89,14 @@ func testNoopApplyChanges(t *testing.T) { }, } - p.ApplyChanges(&plan.Changes{ + ctx := context.Background() + p.ApplyChanges(ctx, &plan.Changes{ Create: providerRecords, }) // wrong changes r, _ := NewNoopRegistry(p) - err := r.ApplyChanges(&plan.Changes{ + err := r.ApplyChanges(ctx, &plan.Changes{ Create: []*endpoint.Endpoint{ { DNSName: "example.org", @@ -106,7 +108,7 @@ func testNoopApplyChanges(t *testing.T) { assert.EqualError(t, err, provider.ErrRecordAlreadyExists.Error()) //correct changes - require.NoError(t, r.ApplyChanges(&plan.Changes{ + require.NoError(t, r.ApplyChanges(ctx, &plan.Changes{ Create: []*endpoint.Endpoint{ { DNSName: "new-record.org", diff --git a/registry/registry.go b/registry/registry.go index 528d4ecff..71e926341 100644 --- a/registry/registry.go +++ b/registry/registry.go @@ -17,6 +17,8 @@ limitations under the License. package registry import ( + "context" + "github.com/kubernetes-incubator/external-dns/endpoint" "github.com/kubernetes-incubator/external-dns/plan" log "github.com/sirupsen/logrus" @@ -28,7 +30,7 @@ import ( // ApplyChanges(changes *plan.Changes) propagates the changes to the DNS Provider API and correspondingly updates ownership depending on type of registry being used type Registry interface { Records() ([]*endpoint.Endpoint, error) - ApplyChanges(changes *plan.Changes) error + ApplyChanges(ctx context.Context, changes *plan.Changes) error } //TODO(ideahitme): consider moving this to Plan diff --git a/registry/txt.go b/registry/txt.go index 528930850..5c5c74663 100644 --- a/registry/txt.go +++ b/registry/txt.go @@ -17,6 +17,7 @@ limitations under the License. package registry import ( + "context" "errors" "time" @@ -117,7 +118,7 @@ func (im *TXTRegistry) Records() ([]*endpoint.Endpoint, error) { // ApplyChanges updates dns provider with the changes // for each created/deleted record it will also take into account TXT records for creation/deletion -func (im *TXTRegistry) ApplyChanges(changes *plan.Changes) error { +func (im *TXTRegistry) ApplyChanges(ctx context.Context, changes *plan.Changes) error { filteredChanges := &plan.Changes{ Create: changes.Create, UpdateNew: filterOwnedRecords(im.ownerID, changes.UpdateNew), @@ -171,7 +172,11 @@ func (im *TXTRegistry) ApplyChanges(changes *plan.Changes) error { } } - return im.provider.ApplyChanges(filteredChanges) + // when caching is enabled, disable the provider from using the cache + if im.cacheInterval > 0 { + ctx = context.WithValue(ctx, provider.RecordsContextKey, nil) + } + return im.provider.ApplyChanges(ctx, filteredChanges) } /** diff --git a/registry/txt_test.go b/registry/txt_test.go index 6ead32165..4489fc864 100644 --- a/registry/txt_test.go +++ b/registry/txt_test.go @@ -17,6 +17,7 @@ limitations under the License. package registry import ( + "context" "reflect" "testing" "time" @@ -68,7 +69,7 @@ func testTXTRegistryRecords(t *testing.T) { func testTXTRegistryRecordsPrefixed(t *testing.T) { p := provider.NewInMemoryProvider() p.CreateZone(testZone) - p.ApplyChanges(&plan.Changes{ + p.ApplyChanges(context.Background(), &plan.Changes{ Create: []*endpoint.Endpoint{ newEndpointWithOwner("foo.test-zone.example.org", "foo.loadbalancer.com", endpoint.RecordTypeCNAME, ""), newEndpointWithOwner("bar.test-zone.example.org", "my-domain.com", endpoint.RecordTypeCNAME, ""), @@ -141,7 +142,7 @@ func testTXTRegistryRecordsPrefixed(t *testing.T) { func testTXTRegistryRecordsNoPrefix(t *testing.T) { p := provider.NewInMemoryProvider() p.CreateZone(testZone) - p.ApplyChanges(&plan.Changes{ + p.ApplyChanges(context.Background(), &plan.Changes{ Create: []*endpoint.Endpoint{ newEndpointWithOwner("foo.test-zone.example.org", "foo.loadbalancer.com", endpoint.RecordTypeCNAME, ""), newEndpointWithOwner("bar.test-zone.example.org", "my-domain.com", endpoint.RecordTypeCNAME, ""), @@ -220,7 +221,12 @@ func testTXTRegistryApplyChanges(t *testing.T) { func testTXTRegistryApplyChangesWithPrefix(t *testing.T) { p := provider.NewInMemoryProvider() p.CreateZone(testZone) - p.ApplyChanges(&plan.Changes{ + ctxEndpoints := []*endpoint.Endpoint{} + ctx := context.WithValue(context.Background(), provider.RecordsContextKey, ctxEndpoints) + p.OnApplyChanges = func(ctx context.Context, got *plan.Changes) { + assert.Equal(t, ctxEndpoints, ctx.Value(provider.RecordsContextKey)) + } + p.ApplyChanges(ctx, &plan.Changes{ Create: []*endpoint.Endpoint{ newEndpointWithOwner("foo.test-zone.example.org", "foo.loadbalancer.com", endpoint.RecordTypeCNAME, ""), newEndpointWithOwner("bar.test-zone.example.org", "my-domain.com", endpoint.RecordTypeCNAME, ""), @@ -267,7 +273,7 @@ func testTXTRegistryApplyChangesWithPrefix(t *testing.T) { newEndpointWithOwner("txt.tar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""), }, } - p.OnApplyChanges = func(got *plan.Changes) { + p.OnApplyChanges = func(ctx context.Context, got *plan.Changes) { mExpected := map[string][]*endpoint.Endpoint{ "Create": expected.Create, "UpdateNew": expected.UpdateNew, @@ -281,15 +287,21 @@ func testTXTRegistryApplyChangesWithPrefix(t *testing.T) { "Delete": got.Delete, } assert.True(t, testutils.SamePlanChanges(mGot, mExpected)) + assert.Equal(t, nil, ctx.Value(provider.RecordsContextKey)) } - err := r.ApplyChanges(changes) + err := r.ApplyChanges(ctx, changes) require.NoError(t, err) } func testTXTRegistryApplyChangesNoPrefix(t *testing.T) { p := provider.NewInMemoryProvider() p.CreateZone(testZone) - p.ApplyChanges(&plan.Changes{ + ctxEndpoints := []*endpoint.Endpoint{} + ctx := context.WithValue(context.Background(), provider.RecordsContextKey, ctxEndpoints) + p.OnApplyChanges = func(ctx context.Context, got *plan.Changes) { + assert.Equal(t, ctxEndpoints, ctx.Value(provider.RecordsContextKey)) + } + p.ApplyChanges(ctx, &plan.Changes{ Create: []*endpoint.Endpoint{ newEndpointWithOwner("foo.test-zone.example.org", "foo.loadbalancer.com", endpoint.RecordTypeCNAME, ""), newEndpointWithOwner("bar.test-zone.example.org", "my-domain.com", endpoint.RecordTypeCNAME, ""), @@ -330,7 +342,7 @@ func testTXTRegistryApplyChangesNoPrefix(t *testing.T) { UpdateNew: []*endpoint.Endpoint{}, UpdateOld: []*endpoint.Endpoint{}, } - p.OnApplyChanges = func(got *plan.Changes) { + p.OnApplyChanges = func(ctx context.Context, got *plan.Changes) { mExpected := map[string][]*endpoint.Endpoint{ "Create": expected.Create, "UpdateNew": expected.UpdateNew, @@ -344,8 +356,9 @@ func testTXTRegistryApplyChangesNoPrefix(t *testing.T) { "Delete": got.Delete, } assert.True(t, testutils.SamePlanChanges(mGot, mExpected)) + assert.Equal(t, nil, ctx.Value(provider.RecordsContextKey)) } - err := r.ApplyChanges(changes) + err := r.ApplyChanges(ctx, changes) require.NoError(t, err) } diff --git a/source/cloudfoundry.go b/source/cloudfoundry.go new file mode 100644 index 000000000..8d8270c4c --- /dev/null +++ b/source/cloudfoundry.go @@ -0,0 +1,59 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package source + +import ( + "net/url" + + cfclient "github.com/cloudfoundry-community/go-cfclient" + "github.com/kubernetes-incubator/external-dns/endpoint" +) + +type cloudfoundrySource struct { + client *cfclient.Client + config Config +} + +// NewCloudFoundrySource creates a new cloudfoundrySource with the given config +func NewCloudFoundrySource(cfClient *cfclient.Client) (Source, error) { + return &cloudfoundrySource{ + client: cfClient, + }, nil +} + +// Endpoints returns endpoint objects +func (rs *cloudfoundrySource) Endpoints() ([]*endpoint.Endpoint, error) { + endpoints := []*endpoint.Endpoint{} + + u, err := url.Parse(rs.client.Config.ApiAddress) + if err != nil { + panic(err) + } + + domains, _ := rs.client.ListDomains() + for _, domain := range domains { + q := url.Values{} + q.Set("q", "domain_guid:"+domain.Guid) + routes, _ := rs.client.ListRoutesByQuery(q) + for _, element := range routes { + endpoints = append(endpoints, + endpoint.NewEndpointWithTTL(element.Host+"."+domain.Name, endpoint.RecordTypeCNAME, 300, u.Host)) + } + } + + return endpoints, nil +} diff --git a/source/cloudfoundry_test.go b/source/cloudfoundry_test.go new file mode 100644 index 000000000..59efc8e6e --- /dev/null +++ b/source/cloudfoundry_test.go @@ -0,0 +1,38 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package source + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" +) + +type RouteSuite struct { + suite.Suite +} + +func TestRouteSource(t *testing.T) { + suite.Run(t, new(RouteSuite)) + t.Run("Interface", testRouteSourceImplementsSource) +} + +// testRouteSourceImplementsSource tests that cloudfoundrySource is a valid Source. +func testRouteSourceImplementsSource(t *testing.T) { + require.Implements(t, (*Source)(nil), new(cloudfoundrySource)) +} diff --git a/source/empty.go b/source/empty.go new file mode 100644 index 000000000..c219581d3 --- /dev/null +++ b/source/empty.go @@ -0,0 +1,32 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package source + +import "github.com/kubernetes-incubator/external-dns/endpoint" + +// emptySource is a Source that returns no endpoints. +type emptySource struct{} + +// Endpoints collects endpoints of all nested Sources and returns them in a single slice. +func (e *emptySource) Endpoints() ([]*endpoint.Endpoint, error) { + return []*endpoint.Endpoint{}, nil +} + +// NewEmptySource creates a new emptySource. +func NewEmptySource() Source { + return &emptySource{} +} diff --git a/source/empty_test.go b/source/empty_test.go new file mode 100644 index 000000000..f82f1a39d --- /dev/null +++ b/source/empty_test.go @@ -0,0 +1,35 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package source + +import ( + "testing" +) + +func TestEmptySourceReturnsEmpty(t *testing.T) { + e := NewEmptySource() + + endpoints, err := e.Endpoints() + if err != nil { + t.Errorf("Expected no error but got %s", err.Error()) + } + + count := len(endpoints) + if count != 0 { + t.Errorf("Expected 0 endpoints but got %d", count) + } +} diff --git a/source/service.go b/source/service.go index 0df1d469d..dcc8f9f26 100644 --- a/source/service.go +++ b/source/service.go @@ -30,7 +30,6 @@ import ( log "github.com/sirupsen/logrus" v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/util/wait" @@ -163,12 +162,6 @@ func (sc *serviceSource) Endpoints() ([]*endpoint.Endpoint, error) { services = sc.filterByServiceType(services) } - // get the ip addresses of all the nodes and cache them for this run - nodeTargets, err := sc.extractNodeTargets() - if err != nil { - return nil, err - } - endpoints := []*endpoint.Endpoint{} for _, svc := range services { @@ -180,7 +173,7 @@ func (sc *serviceSource) Endpoints() ([]*endpoint.Endpoint, error) { continue } - svcEndpoints := sc.endpoints(svc, nodeTargets) + svcEndpoints := sc.endpoints(svc) // process legacy annotations if no endpoints were returned and compatibility mode is enabled. if len(svcEndpoints) == 0 && sc.compatibility != "" { @@ -189,7 +182,7 @@ func (sc *serviceSource) Endpoints() ([]*endpoint.Endpoint, error) { // apply template if none of the above is found if (sc.combineFQDNAnnotation || len(svcEndpoints) == 0) && sc.fqdnTemplate != nil { - sEndpoints, err := sc.endpointsFromTemplate(svc, nodeTargets) + sEndpoints, err := sc.endpointsFromTemplate(svc) if err != nil { return nil, err } @@ -238,26 +231,28 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri targetsByHeadlessDomain := make(map[string][]string) for _, v := range pods { - headlessDomain := hostname - if v.Spec.Hostname != "" { - headlessDomain = v.Spec.Hostname + "." + headlessDomain - } + headlessDomains := []string{hostname} - if sc.publishHostIP == true { - log.Debugf("Generating matching endpoint %s with HostIP %s", headlessDomain, v.Status.HostIP) - // To reduce traffice on the DNS API only add record for running Pods. Good Idea? - if v.Status.Phase == v1.PodRunning { - targetsByHeadlessDomain[headlessDomain] = append(targetsByHeadlessDomain[headlessDomain], v.Status.HostIP) + if v.Spec.Hostname != "" { + headlessDomains = append(headlessDomains, fmt.Sprintf("%s.%s", v.Spec.Hostname, hostname)) + } + for _, headlessDomain := range headlessDomains { + if sc.publishHostIP == true { + log.Debugf("Generating matching endpoint %s with HostIP %s", headlessDomain, v.Status.HostIP) + // To reduce traffice on the DNS API only add record for running Pods. Good Idea? + if v.Status.Phase == v1.PodRunning { + targetsByHeadlessDomain[headlessDomain] = append(targetsByHeadlessDomain[headlessDomain], v.Status.HostIP) + } else { + log.Debugf("Pod %s is not in running phase", v.Spec.Hostname) + } } else { - log.Debugf("Pod %s is not in running phase", v.Spec.Hostname) - } - } else { - log.Debugf("Generating matching endpoint %s with PodIP %s", headlessDomain, v.Status.PodIP) - // To reduce traffice on the DNS API only add record for running Pods. Good Idea? - if v.Status.Phase == v1.PodRunning { - targetsByHeadlessDomain[headlessDomain] = append(targetsByHeadlessDomain[headlessDomain], v.Status.PodIP) - } else { - log.Debugf("Pod %s is not in running phase", v.Spec.Hostname) + log.Debugf("Generating matching endpoint %s with PodIP %s", headlessDomain, v.Status.PodIP) + // To reduce traffice on the DNS API only add record for running Pods. Good Idea? + if v.Status.Phase == v1.PodRunning { + targetsByHeadlessDomain[headlessDomain] = append(targetsByHeadlessDomain[headlessDomain], v.Status.PodIP) + } else { + log.Debugf("Pod %s is not in running phase", v.Spec.Hostname) + } } } @@ -280,7 +275,7 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri return endpoints } -func (sc *serviceSource) endpointsFromTemplate(svc *v1.Service, nodeTargets endpoint.Targets) ([]*endpoint.Endpoint, error) { +func (sc *serviceSource) endpointsFromTemplate(svc *v1.Service) ([]*endpoint.Endpoint, error) { var endpoints []*endpoint.Endpoint // Process the whole template string @@ -293,21 +288,21 @@ func (sc *serviceSource) endpointsFromTemplate(svc *v1.Service, nodeTargets endp providerSpecific := getProviderSpecificAnnotations(svc.Annotations) hostnameList := strings.Split(strings.Replace(buf.String(), " ", "", -1), ",") for _, hostname := range hostnameList { - endpoints = append(endpoints, sc.generateEndpoints(svc, hostname, nodeTargets, providerSpecific)...) + endpoints = append(endpoints, sc.generateEndpoints(svc, hostname, providerSpecific)...) } return endpoints, nil } // endpointsFromService extracts the endpoints from a service object -func (sc *serviceSource) endpoints(svc *v1.Service, nodeTargets endpoint.Targets) []*endpoint.Endpoint { +func (sc *serviceSource) endpoints(svc *v1.Service) []*endpoint.Endpoint { var endpoints []*endpoint.Endpoint // Skip endpoints if we do not want entries from annotations if !sc.ignoreHostnameAnnotation { providerSpecific := getProviderSpecificAnnotations(svc.Annotations) hostnameList := getHostnamesFromAnnotations(svc.Annotations) for _, hostname := range hostnameList { - endpoints = append(endpoints, sc.generateEndpoints(svc, hostname, nodeTargets, providerSpecific)...) + endpoints = append(endpoints, sc.generateEndpoints(svc, hostname, providerSpecific)...) } } return endpoints @@ -363,7 +358,7 @@ func (sc *serviceSource) setResourceLabel(service *v1.Service, endpoints []*endp } } -func (sc *serviceSource) generateEndpoints(svc *v1.Service, hostname string, nodeTargets endpoint.Targets, providerSpecific endpoint.ProviderSpecific) []*endpoint.Endpoint { +func (sc *serviceSource) generateEndpoints(svc *v1.Service, hostname string, providerSpecific endpoint.ProviderSpecific) []*endpoint.Endpoint { hostname = strings.TrimSuffix(hostname, ".") ttl, err := getTTLFromAnnotations(svc.Annotations) if err != nil { @@ -403,9 +398,13 @@ func (sc *serviceSource) generateEndpoints(svc *v1.Service, hostname string, nod } case v1.ServiceTypeNodePort: // add the nodeTargets and extract an SRV endpoint - targets = append(targets, nodeTargets...) - endpoints = append(endpoints, sc.extractNodePortEndpoints(svc, nodeTargets, hostname, ttl)...) - case v1.ServiceTypeExternalName: + targets, err = sc.extractNodePortTargets(svc) + if err != nil { + log.Errorf("Unable to extract targets from service %s/%s error: %v", svc.Namespace, svc.Name, err) + return endpoints + } + endpoints = append(endpoints, sc.extractNodePortEndpoints(svc, targets, hostname, ttl)...) + case v1.ServiceTypeExternalName: targets = append(targets, extractServiceExternalName(svc)...) } @@ -455,20 +454,44 @@ func extractLoadBalancerTargets(svc *v1.Service) endpoint.Targets { return targets } -func (sc *serviceSource) extractNodeTargets() (endpoint.Targets, error) { +func (sc *serviceSource) extractNodePortTargets(svc *v1.Service) (endpoint.Targets, error) { var ( internalIPs endpoint.Targets externalIPs endpoint.Targets + nodes []*v1.Node + err error ) - nodes, err := sc.nodeInformer.Lister().List(labels.Everything()) - if err != nil { - if errors.IsForbidden(err) { - // Return an empty list because it makes sense to continue and try other sources. - log.Debugf("Unable to list nodes (Forbidden), returning empty list of targets (NodePort services will be skipped)") - return endpoint.Targets{}, nil + switch svc.Spec.ExternalTrafficPolicy { + case v1.ServiceExternalTrafficPolicyTypeLocal: + labelSelector, err := metav1.ParseToLabelSelector(labels.Set(svc.Spec.Selector).AsSelectorPreValidated().String()) + if err != nil { + return nil, err + } + selector, err := metav1.LabelSelectorAsSelector(labelSelector) + if err != nil { + return nil, err + } + pods, err := sc.podInformer.Lister().Pods(svc.Namespace).List(selector) + if err != nil { + return nil, err + } + + for _, v := range pods { + if v.Status.Phase == v1.PodRunning { + node, err := sc.nodeInformer.Lister().Get(v.Spec.NodeName) + if err != nil { + log.Debugf("Unable to find node where Pod %s is running", v.Spec.Hostname) + continue + } + nodes = append(nodes, node) + } + } + default: + nodes, err = sc.nodeInformer.Lister().List(labels.Everything()) + if err != nil { + return nil, err } - return nil, err } for _, node := range nodes { diff --git a/source/service_test.go b/source/service_test.go index 5c45a2154..b5265e339 100644 --- a/source/service_test.go +++ b/source/service_test.go @@ -1282,6 +1282,7 @@ func TestNodePortServices(t *testing.T) { svcNamespace string svcName string svcType v1.ServiceType + svcTrafficPolicy v1.ServiceExternalTrafficPolicyType compatibility string fqdnTemplate string ignoreHostnameAnnotation bool @@ -1291,6 +1292,9 @@ func TestNodePortServices(t *testing.T) { expected []*endpoint.Endpoint expectError bool nodes []*v1.Node + podnames []string + nodeIndex []int + phases []v1.PodPhase }{ { "annotated NodePort services return an endpoint with IP addresses of the cluster's nodes", @@ -1299,6 +1303,7 @@ func TestNodePortServices(t *testing.T) { "testing", "foo", v1.ServiceTypeNodePort, + v1.ServiceExternalTrafficPolicyTypeCluster, "", "", false, @@ -1333,6 +1338,9 @@ func TestNodePortServices(t *testing.T) { }, }, }}, + []string{}, + []int{}, + []v1.PodPhase{}, }, { "hostname annotated NodePort services are ignored", @@ -1341,6 +1349,7 @@ func TestNodePortServices(t *testing.T) { "testing", "foo", v1.ServiceTypeNodePort, + v1.ServiceExternalTrafficPolicyTypeCluster, "", "", true, @@ -1372,6 +1381,9 @@ func TestNodePortServices(t *testing.T) { }, }, }}, + []string{}, + []int{}, + []v1.PodPhase{}, }, { "non-annotated NodePort services with set fqdnTemplate return an endpoint with target IP", @@ -1380,6 +1392,7 @@ func TestNodePortServices(t *testing.T) { "testing", "foo", v1.ServiceTypeNodePort, + v1.ServiceExternalTrafficPolicyTypeCluster, "", "{{.Name}}.bar.example.com", false, @@ -1412,6 +1425,9 @@ func TestNodePortServices(t *testing.T) { }, }, }}, + []string{}, + []int{}, + []v1.PodPhase{}, }, { "annotated NodePort services return an endpoint with IP addresses of the private cluster's nodes", @@ -1420,6 +1436,7 @@ func TestNodePortServices(t *testing.T) { "testing", "foo", v1.ServiceTypeNodePort, + v1.ServiceExternalTrafficPolicyTypeCluster, "", "", false, @@ -1452,6 +1469,55 @@ func TestNodePortServices(t *testing.T) { }, }, }}, + []string{}, + []int{}, + []v1.PodPhase{}, + }, + { + "annotated NodePort services with ExternalTrafficPolicy=Local return an endpoint with IP addresses of the cluster's nodes where pods is running only", + "", + "", + "testing", + "foo", + v1.ServiceTypeNodePort, + v1.ServiceExternalTrafficPolicyTypeLocal, + "", + "", + false, + map[string]string{}, + map[string]string{ + hostnameAnnotationKey: "foo.example.org.", + }, + nil, + []*endpoint.Endpoint{ + {DNSName: "_30192._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV}, + {DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.2"}, RecordType: endpoint.RecordTypeA}, + }, + false, + []*v1.Node{{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node1", + }, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + {Type: v1.NodeExternalIP, Address: "54.10.11.1"}, + {Type: v1.NodeInternalIP, Address: "10.0.1.1"}, + }, + }, + }, { + ObjectMeta: metav1.ObjectMeta{ + Name: "node2", + }, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + {Type: v1.NodeExternalIP, Address: "54.10.11.2"}, + {Type: v1.NodeInternalIP, Address: "10.0.1.2"}, + }, + }, + }}, + []string{"master-0"}, + []int{1}, + []v1.PodPhase{v1.PodRunning}, }, } { t.Run(tc.title, func(t *testing.T) { @@ -1465,10 +1531,34 @@ func TestNodePortServices(t *testing.T) { } } + // Create pods + for i, podname := range tc.podnames { + pod := &v1.Pod{ + Spec: v1.PodSpec{ + Containers: []v1.Container{}, + Hostname: podname, + NodeName: tc.nodes[tc.nodeIndex[i]].Name, + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: tc.svcNamespace, + Name: podname, + Labels: tc.labels, + Annotations: tc.annotations, + }, + Status: v1.PodStatus{ + Phase: tc.phases[i], + }, + } + + _, err := kubernetes.CoreV1().Pods(tc.svcNamespace).Create(pod) + require.NoError(t, err) + } + // Create a service to test against service := &v1.Service{ Spec: v1.ServiceSpec{ - Type: tc.svcType, + Type: tc.svcType, + ExternalTrafficPolicy: tc.svcTrafficPolicy, Ports: []v1.ServicePort{ { NodePort: 30192, @@ -1562,6 +1652,7 @@ func TestHeadlessServices(t *testing.T) { []*endpoint.Endpoint{ {DNSName: "foo-0.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}}, {DNSName: "foo-1.service.example.org", Targets: endpoint.Targets{"1.1.1.2"}}, + {DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}}, }, false, }, @@ -1616,6 +1707,7 @@ func TestHeadlessServices(t *testing.T) { []*endpoint.Endpoint{ {DNSName: "foo-0.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}, RecordTTL: endpoint.TTL(1)}, {DNSName: "foo-1.service.example.org", Targets: endpoint.Targets{"1.1.1.2"}, RecordTTL: endpoint.TTL(1)}, + {DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}, RecordTTL: endpoint.TTL(1)}, }, false, }, @@ -1643,6 +1735,7 @@ func TestHeadlessServices(t *testing.T) { []v1.PodPhase{v1.PodRunning, v1.PodFailed}, []*endpoint.Endpoint{ {DNSName: "foo-0.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}}, + {DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1"}}, }, false, }, @@ -1793,6 +1886,7 @@ func TestHeadlessServicesHostIP(t *testing.T) { []*endpoint.Endpoint{ {DNSName: "foo-0.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}}, {DNSName: "foo-1.service.example.org", Targets: endpoint.Targets{"1.1.1.2"}}, + {DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}}, }, false, }, @@ -1847,6 +1941,7 @@ func TestHeadlessServicesHostIP(t *testing.T) { []*endpoint.Endpoint{ {DNSName: "foo-0.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}, RecordTTL: endpoint.TTL(1)}, {DNSName: "foo-1.service.example.org", Targets: endpoint.Targets{"1.1.1.2"}, RecordTTL: endpoint.TTL(1)}, + {DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}, RecordTTL: endpoint.TTL(1)}, }, false, }, @@ -1874,6 +1969,7 @@ func TestHeadlessServicesHostIP(t *testing.T) { []v1.PodPhase{v1.PodRunning, v1.PodFailed}, []*endpoint.Endpoint{ {DNSName: "foo-0.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}}, + {DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1"}}, }, false, }, diff --git a/source/store.go b/source/store.go index 4a143e13d..5491085e8 100644 --- a/source/store.go +++ b/source/store.go @@ -25,6 +25,7 @@ import ( "sync" + cfclient "github.com/cloudfoundry-community/go-cfclient" "github.com/linki/instrumented_http" log "github.com/sirupsen/logrus" istiocrd "istio.io/istio/pilot/pkg/config/kube/crd" @@ -53,12 +54,16 @@ type Config struct { KubeMaster string ServiceTypeFilter []string IstioIngressGatewayServices []string + CFAPIEndpoint string + CFUsername string + CFPassword string } // ClientGenerator provides clients type ClientGenerator interface { KubeClient() (kubernetes.Interface, error) IstioClient() (istiomodel.ConfigStore, error) + CloudFoundryClient(cfAPPEndpoint string, cfUsername string, cfPassword string) (*cfclient.Client, error) } // SingletonClientGenerator stores provider clients and guarantees that only one instance of client @@ -69,8 +74,10 @@ type SingletonClientGenerator struct { RequestTimeout time.Duration kubeClient kubernetes.Interface istioClient istiomodel.ConfigStore + cfClient *cfclient.Client kubeOnce sync.Once istioOnce sync.Once + cfOnce sync.Once } // KubeClient generates a kube client if it was not created before @@ -91,6 +98,30 @@ func (p *SingletonClientGenerator) IstioClient() (istiomodel.ConfigStore, error) return p.istioClient, err } +// CloudFoundryClient generates a cf client if it was not created before +func (p *SingletonClientGenerator) CloudFoundryClient(cfAPIEndpoint string, cfUsername string, cfPassword string) (*cfclient.Client, error) { + var err error + p.cfOnce.Do(func() { + p.cfClient, err = NewCFClient(cfAPIEndpoint, cfUsername, cfPassword) + }) + return p.cfClient, err +} + +// NewCFClient return a new CF client object. +func NewCFClient(cfAPIEndpoint string, cfUsername string, cfPassword string) (*cfclient.Client, error) { + c := &cfclient.Config{ + ApiAddress: "https://" + cfAPIEndpoint, + Username: cfUsername, + Password: cfPassword, + } + client, err := cfclient.NewClient(c) + if err != nil { + return nil, err + } + + return client, nil +} + // ByNames returns multiple Sources given multiple names. func ByNames(p ClientGenerator, names []string, cfg *Config) ([]Source, error) { sources := []Source{} @@ -130,6 +161,12 @@ func BuildWithConfig(source string, p ClientGenerator, cfg *Config) (Source, err return nil, err } return NewIstioGatewaySource(kubernetesClient, istioClient, cfg.IstioIngressGatewayServices, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation) + case "cloudfoundry": + cfClient, err := p.CloudFoundryClient(cfg.CFAPIEndpoint, cfg.CFUsername, cfg.CFPassword) + if err != nil { + return nil, err + } + return NewCloudFoundrySource(cfClient) case "fake": return NewFakeSource(cfg.FQDNTemplate) case "connector": diff --git a/source/store_test.go b/source/store_test.go index 08045a0a8..176ac9bbd 100644 --- a/source/store_test.go +++ b/source/store_test.go @@ -24,14 +24,16 @@ import ( "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/fake" + cfclient "github.com/cloudfoundry-community/go-cfclient" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" ) type MockClientGenerator struct { mock.Mock - kubeClient kubernetes.Interface - istioClient istiomodel.ConfigStore + kubeClient kubernetes.Interface + istioClient istiomodel.ConfigStore + cloudFoundryClient *cfclient.Client } func (m *MockClientGenerator) KubeClient() (kubernetes.Interface, error) { @@ -52,6 +54,15 @@ func (m *MockClientGenerator) IstioClient() (istiomodel.ConfigStore, error) { return nil, args.Error(1) } +func (m *MockClientGenerator) CloudFoundryClient(cfAPIEndpoint string, cfUsername string, cfPassword string) (*cfclient.Client, error) { + args := m.Called() + if args.Error(1) == nil { + m.cloudFoundryClient = args.Get(0).(*cfclient.Client) + return m.cloudFoundryClient, nil + } + return nil, args.Error(1) +} + type ByNamesTestSuite struct { suite.Suite }