Merge branch 'master' into cloudflare-proxied-annotation

This commit is contained in:
Nick Jüttner 2019-01-24 11:16:54 +01:00 committed by GitHub
commit 6d39526069
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 1056 additions and 371 deletions

View File

@ -22,6 +22,9 @@ before_install:
install:
- gometalinter --install
env:
- GOMETALINTER_DEADLINE="600s"
script:
- vendor/github.com/kubernetes/repo-infra/verify/verify-boilerplate.sh --rootdir=$(pwd)
- vendor/github.com/kubernetes/repo-infra/verify/verify-go-src.sh -v --rootdir $(pwd)

View File

@ -1,3 +1,28 @@
## v0.5.9 - 2018-11-22
- Core: Update delivery.yaml to new format (#782) @linki
- Core: Adjust gometalinter timeout by setting env var (#778) @njuettner
- Provider **Google**: Panic assignment to entry in nil map (#776) @njuettner
- Docs: Fix typos (#769) @mooncak
- Docs: Remove duplicated words (#768) @mooncak
- Provider **Alibaba**: Alibaba Cloud Provider Fix Multiple Subdomains Bug (#767) @xianlubird
- Core: Add Traefik to the supported list of ingress controllers (#764) @coderanger
- Provider **Dyn**: Fix some typos in returned messages in dyn.go (#760) @AdamDang
- Docs: Update Azure documentation (#756) @pascalgn
- Provider **Oracle**: Oracle doc fix (add "key:" to secret) (#750) @CaptTofu
- Core: Docker MAINTAINER is deprecated - using LABEL instead (#747) @helgi
- Core: Feature add alias annotation (#742) @vaegt
- Provider **RFC2136**: Fix rfc2136 - setup fails issue and small docs (#741) @antlad
- Core: Fix nil map access of endpoint labels (#739) @shashidharatd
- Provider **PowerDNS**: PowerDNS Add DomainFilter support (#737) @ottoyiu
- Core: Fix domain-filter matching logic to not match similar domain names (#736) @ottoyiu
- Core: Matching entire string for wildcard in txt records with prefixes (#727) @etopeter
- Provider **Designate**: Fix TLS issue with OpenStack auth (#717) @FestivalBobcats
- Provider **AWS**: Add helper script to update route53 txt owner entries (#697) @efranford
- Provider **CoreDNS**: Migrate to use etcd client v3 for CoreDNS provider (#686) @shashidharatd
- Core: Create a non-root user to run the container process (#684) @coderanger
- Core: Do not replace TXT records with A/CNAME records in planner (#581) @jchv
## v0.5.8 - 2018-10-11
- New Provider: RFC2136 (#702) @antlad

84
Gopkg.lock generated
View File

@ -360,9 +360,12 @@
version = "1.1.4"
[[projects]]
branch = "master"
digest = "1:fd50e2c52f29bb81f9a172f0d5aee1438b201ca0502ff3a20ebbe9629e274875"
name = "github.com/kubernetes/repo-infra"
packages = ["verify/boilerplate/test"]
revision = "c2f9667a4c29e70a39b0e89db2d4f0cab907dbee"
pruneopts = ""
revision = "1bcb110c8726cee477939f507f4760a95e155347"
[[projects]]
name = "github.com/linki/instrumented_http"
@ -477,6 +480,13 @@
]
revision = "a6e9df898b1336106c743392c48ee0b71f5c4efa"
[[projects]]
branch = "master"
name = "github.com/sanyu/dynectsoap"
packages = ["dynectsoap"]
pruneopts = ""
revision = "b83de5edc4e022f22903eeb3b428d2f39fb740e5"
[[projects]]
name = "github.com/satori/go.uuid"
packages = ["."]
@ -942,6 +952,76 @@
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "e4a3503a3672ad47f8e8f73bc26322fd1a01e2887675a444a87fee626f42da42"
input-imports = [
"cloud.google.com/go/compute/metadata",
"github.com/Azure/azure-sdk-for-go/arm/dns",
"github.com/Azure/go-autorest/autorest",
"github.com/Azure/go-autorest/autorest/adal",
"github.com/Azure/go-autorest/autorest/azure",
"github.com/Azure/go-autorest/autorest/to",
"github.com/alecthomas/kingpin",
"github.com/aliyun/alibaba-cloud-sdk-go/sdk/requests",
"github.com/aliyun/alibaba-cloud-sdk-go/services/alidns",
"github.com/aliyun/alibaba-cloud-sdk-go/services/pvtz",
"github.com/aws/aws-sdk-go/aws",
"github.com/aws/aws-sdk-go/aws/credentials/stscreds",
"github.com/aws/aws-sdk-go/aws/request",
"github.com/aws/aws-sdk-go/aws/session",
"github.com/aws/aws-sdk-go/service/route53",
"github.com/aws/aws-sdk-go/service/servicediscovery",
"github.com/cloudflare/cloudflare-go",
"github.com/coreos/etcd/clientv3",
"github.com/denverdino/aliyungo/metadata",
"github.com/digitalocean/godo",
"github.com/digitalocean/godo/context",
"github.com/dnsimple/dnsimple-go/dnsimple",
"github.com/exoscale/egoscale",
"github.com/ffledgling/pdns-go",
"github.com/gophercloud/gophercloud",
"github.com/gophercloud/gophercloud/openstack",
"github.com/gophercloud/gophercloud/openstack/dns/v2/recordsets",
"github.com/gophercloud/gophercloud/openstack/dns/v2/zones",
"github.com/gophercloud/gophercloud/pagination",
"github.com/infobloxopen/infoblox-go-client",
"github.com/kubernetes/repo-infra/verify/boilerplate/test",
"github.com/linki/instrumented_http",
"github.com/linode/linodego",
"github.com/miekg/dns",
"github.com/nesv/go-dynect/dynect",
"github.com/oracle/oci-go-sdk/common",
"github.com/oracle/oci-go-sdk/dns",
"github.com/pkg/errors",
"github.com/prometheus/client_golang/prometheus",
"github.com/prometheus/client_golang/prometheus/promhttp",
"github.com/sanyu/dynectsoap/dynectsoap",
"github.com/sirupsen/logrus",
"github.com/stretchr/testify/assert",
"github.com/stretchr/testify/mock",
"github.com/stretchr/testify/require",
"github.com/stretchr/testify/suite",
"golang.org/x/net/context",
"golang.org/x/oauth2",
"golang.org/x/oauth2/google",
"google.golang.org/api/dns/v1",
"google.golang.org/api/googleapi",
"gopkg.in/yaml.v2",
"istio.io/api/networking/v1alpha3",
"istio.io/istio/pilot/pkg/config/kube/crd",
"istio.io/istio/pilot/pkg/model",
"k8s.io/api/core/v1",
"k8s.io/api/extensions/v1beta1",
"k8s.io/apimachinery/pkg/api/errors",
"k8s.io/apimachinery/pkg/apis/meta/v1",
"k8s.io/apimachinery/pkg/labels",
"k8s.io/apimachinery/pkg/runtime",
"k8s.io/apimachinery/pkg/runtime/schema",
"k8s.io/apimachinery/pkg/runtime/serializer",
"k8s.io/client-go/kubernetes",
"k8s.io/client-go/kubernetes/fake",
"k8s.io/client-go/plugin/pkg/client/auth",
"k8s.io/client-go/rest",
"k8s.io/client-go/rest/fake",
"k8s.io/client-go/tools/clientcmd",
]
solver-name = "gps-cdcl"
solver-version = 1

View File

@ -54,7 +54,7 @@ ignored = ["github.com/kubernetes/repo-infra/kazel"]
[[override]]
name = "github.com/kubernetes/repo-infra"
revision = "c2f9667a4c29e70a39b0e89db2d4f0cab907dbee"
branch = "master"
[[constraint]]
name = "github.com/nesv/go-dynect"
@ -119,3 +119,7 @@ ignored = ["github.com/kubernetes/repo-infra/kazel"]
[[constraint]]
name = "github.com/google/go-cmp"
version = "0.2.0"
[constraint]]
name = "github.com/sanyu/dynectsoap"
branch = "master"

View File

@ -13,7 +13,7 @@ ExternalDNS synchronizes exposed Kubernetes Services and Ingresses with DNS prov
## What It Does
Inspired by [Kubernetes DNS](https://github.com/kubernetes/dns), Kubernetes' cluster-internal DNS server, ExternalDNS makes Kubernetes resources discoverable via public DNS servers. Like KubeDNS, it retrieves a list of resources (Services, Ingresses, etc.) from the [Kubernetes API](https://kubernetes.io/docs/api/) to determine a desired list of DNS records. *Unlike* KubeDNS, however, it's not a DNS server itself, but merely configures other DNS providers accordingly—e.g. [AWS Route 53](https://aws.amazon.com/route53/) or [Google CloudDNS](https://cloud.google.com/dns/docs/).
Inspired by [Kubernetes DNS](https://github.com/kubernetes/dns), Kubernetes' cluster-internal DNS server, ExternalDNS makes Kubernetes resources discoverable via public DNS servers. Like KubeDNS, it retrieves a list of resources (Services, Ingresses, etc.) from the [Kubernetes API](https://kubernetes.io/docs/api/) to determine a desired list of DNS records. *Unlike* KubeDNS, however, it's not a DNS server itself, but merely configures other DNS providers accordingly—e.g. [AWS Route 53](https://aws.amazon.com/route53/) or [Google Cloud DNS](https://cloud.google.com/dns/docs/).
In a broader sense, ExternalDNS allows you to control DNS records dynamically via Kubernetes resources in a DNS provider-agnostic way.
@ -24,7 +24,7 @@ To see ExternalDNS in action, have a look at this [video](https://www.youtube.co
## The Latest Release: v0.5
ExternalDNS' current release is `v0.5`. This version allows you to keep selected zones (via `--domain-filter`) synchronized with Ingresses and Services of `type=LoadBalancer` in various cloud providers:
* [Google CloudDNS](https://cloud.google.com/dns/docs/)
* [Google Cloud DNS](https://cloud.google.com/dns/docs/)
* [AWS Route 53](https://aws.amazon.com/route53/)
* [AWS Service Discovery](https://docs.aws.amazon.com/Route53/latest/APIReference/overview-service-discovery.html)
* [AzureDNS](https://azure.microsoft.com/en-us/services/dns)
@ -55,6 +55,7 @@ The following tutorials are provided:
* [AWS (Route53)](docs/tutorials/aws.md)
* [AWS (Service Discovery)](docs/tutorials/aws-sd.md)
* [Azure](docs/tutorials/azure.md)
* [CoreDNS](docs/tutorials/coredns.md)
* [Cloudflare](docs/tutorials/cloudflare.md)
* [DigitalOcean](docs/tutorials/digitalocean.md)
* [Infoblox](docs/tutorials/infoblox.md)
@ -178,6 +179,7 @@ Here's a rough outline on what is to come (subject to change):
- [x] Support for creating DNS records to multiple targets (for Google and AWS)
- [x] Support for OpenStack Designate
- [x] Support for PowerDNS
- [x] Support for Linode
### v0.6

16
SECURITY_CONTACTS Normal file
View File

@ -0,0 +1,16 @@
# Defined below are the security contacts for this repo.
#
# They are the contact point for the Product Security Team to reach out
# to for triaging and handling of incoming issues.
#
# The below names agree to abide by the
# [Embargo Policy](https://github.com/kubernetes/sig-release/blob/master/security-release-process-documentation/security-release-process.md#embargo-policy)
# and will be removed and replaced if they violate that agreement.
#
# DO NOT REPORT SECURITY VULNERABILITIES DIRECTLY TO THESE NAMES, FOLLOW THE
# INSTRUCTIONS AT https://kubernetes.io/security/
linki
njuettner
hjacobs
raffo

View File

@ -40,11 +40,29 @@ var (
Help: "Number of Source errors.",
},
)
sourceEndpointsTotal = prometheus.NewGauge(
prometheus.GaugeOpts{
Namespace: "external_dns",
Subsystem: "source",
Name: "endpoints_total",
Help: "Number of Endpoints in all sources",
},
)
registryEndpointsTotal = prometheus.NewGauge(
prometheus.GaugeOpts{
Namespace: "external_dns",
Subsystem: "registry",
Name: "endpoints_total",
Help: "Number of Endpoints in the registry",
},
)
)
func init() {
prometheus.MustRegister(registryErrors)
prometheus.MustRegister(sourceErrors)
prometheus.MustRegister(sourceEndpointsTotal)
prometheus.MustRegister(registryEndpointsTotal)
}
// Controller is responsible for orchestrating the different components.
@ -69,12 +87,14 @@ func (c *Controller) RunOnce() error {
registryErrors.Inc()
return err
}
registryEndpointsTotal.Set(float64(len(records)))
endpoints, err := c.Source.Endpoints()
if err != nil {
sourceErrors.Inc()
return err
}
sourceEndpointsTotal.Set(float64(len(endpoints)))
plan := &plan.Plan{
Policies: []plan.Policy{c.Policy},

View File

@ -1,19 +1,15 @@
build_steps:
- desc: Install docker
cmd: |
apt-get update
apt-get install -y apt-transport-https ca-certificates curl software-properties-common
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
apt-get update
apt-get install -y docker-ce
- desc: Build and push docker image
cmd: |
if [[ $CDP_TARGET_BRANCH == master && ! $CDP_PULL_REQUEST_NUMBER ]]; then
RELEASE_VERSION=$(git describe --tags --always --dirty)
IMAGE=registry-write.opensource.zalan.do/teapot/external-dns:${RELEASE_VERSION}
else
IMAGE=registry-write.opensource.zalan.do/teapot/external-dns-test:${CDP_BUILD_VERSION}
fi
docker build --squash --tag "$IMAGE" .
docker push "$IMAGE"
version: "2017-09-20"
pipeline:
- id: build
type: script
commands:
- desc: Build and push Docker image
cmd: |
if [[ $CDP_TARGET_BRANCH == master && ! $CDP_PULL_REQUEST_NUMBER ]]; then
RELEASE_VERSION=$(git describe --tags --always --dirty)
IMAGE=registry-write.opensource.zalan.do/teapot/external-dns:${RELEASE_VERSION}
else
IMAGE=registry-write.opensource.zalan.do/teapot/external-dns-test:${CDP_BUILD_VERSION}
fi
docker build --squash --tag "$IMAGE" .
docker push "$IMAGE"

View File

@ -14,7 +14,7 @@ This list of endpoints is passed to the [Plan](../../plan) which determines the
Once the difference has been figured out the list of intended changes is passed to a `Registry` which live in the [registry](../../registry) package. The registry is a wrapper and access point to DNS provider. Registry implements the ownership concept by marking owned records and filtering out records not owned by ExternalDNS before passing them to DNS provider.
The [provider](../../provider) is the adapter to the DNS provider, e.g. Google CloudDNS. It implements two methods: `ApplyChanges` to apply a set of changes filtered by `Registry` and `Records` to retrieve the current list of records from the DNS provider.
The [provider](../../provider) is the adapter to the DNS provider, e.g. Google Cloud DNS. It implements two methods: `ApplyChanges` to apply a set of changes filtered by `Registry` and `Records` to retrieve the current list of records from the DNS provider.
The orchestration between the different components is controlled by the [controller](../../controller).

View File

@ -29,7 +29,7 @@ All sources live in package `source`.
### Providers
Providers are an abstraction over any kind of sink for desired Endpoints, e.g.:
* storing them in Google CloudDNS
* storing them in Google Cloud DNS
* printing them to stdout for testing purposes
* fanning out to multiple nested providers
@ -46,7 +46,7 @@ The interface tries to be generic and assumes a flat list of records for both fu
All providers live in package `provider`.
* `GoogleProvider`: returns and creates DNS records in Google CloudDNS
* `GoogleProvider`: returns and creates DNS records in Google Cloud DNS
* `AWSProvider`: returns and creates DNS records in AWS Route 53
* `AzureProvider`: returns and creates DNS records in Azure DNS
* `InMemoryProvider`: Keeps a list of records in local memory

View File

@ -30,7 +30,7 @@ ExternalDNS can solve this for you as well.
Currently, the following providers are supported:
- Google CloudDNS
- Google Cloud DNS
- AWS Route 53
- AzureDNS
- CloudFlare
@ -40,6 +40,11 @@ Currently, the following providers are supported:
- Dyn
- OpenStack Designate
- PowerDNS
- CoreDNS
- Exoscale
- Oracle Cloud Infrastructure DNS
- Linode DNS
- RFC2136
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.
@ -155,6 +160,18 @@ CNAMEs cannot co-exist with other records, therefore you can use the `--txt-pref
You need to add either https://www.googleapis.com/auth/ndev.clouddns.readwrite or https://www.googleapis.com/auth/cloud-platform on your instance group's scope.
### What metrics can I get from ExternalDNS and what do they mean?
ExternalDNS exposes 2 types of metrics: Sources and Registry errors.
`Source`s are mostly Kubernetes API objects. Examples of `source` errors may be connection errors to the Kubernetes API server itself or missing RBAC permissions. It can also stem from incompatible configuration in the objects itself like invalid characters, processing a broken fqdnTemplate, etc.
`Registry` errors are mostly Provider errors, unless there's some coding flaw in the registry package. Provider errors often arise due to accessing their APIs due to network or missing cloud-provider permissions when reading records. When applying a changeset, errors will arise if the changeset applied is incompatible with the current state.
In case of an increased error count, you could correlate them with the `http_request_duration_seconds{handler="instrumented_http"}` metric which should show increased numbers for status codes 4xx (permissions, configuration, invalid changeset) or 5xx (apiserver down).
You can use the host label in the metric to figure out if the request was against the Kubernetes API server (Source errors) or the DNS provider API (Registry/Provider errors).
### How can I run ExternalDNS under a specific GCP Service Account, e.g. to access DNS records in other projects?
Have a look at https://github.com/linki/mate/blob/v0.6.2/examples/google/README.md#permissions
@ -226,3 +243,11 @@ To do this with ExternalDNS you can use the `--annotation-filter` to specificall
an instance of a ingress controller. Let's assume you have two ingress controllers `nginx-internal` and `nginx-external`
then you can start two ExternalDNS providers one with `--annotation-filter=kubernetes.io/ingress.class=nginx-internal`
and one with `--annotation-filter=kubernetes.io/ingress.class=nginx-external`.
### Can external-dns manage(add/remove) records in a hosted zone which is setup in different aws account.
yes, give it the correct cross-account/assume-role permissions and use the `--aws-assume-role` flag https://github.com/kubernetes-incubator/external-dns/pull/524#issue-181256561
### how do I provide multiple values to the annotation `external-dns.alpha.kubernetes.io/hostname`
separate them by `,`

View File

@ -9,7 +9,7 @@ This tutorial uses [Azure CLI 2.0](https://docs.microsoft.com/en-us/cli/azure/in
Azure commands and assumes that the Kubernetes cluster was created via Azure Container Services and `kubectl` commands
are being run on an orchestration master.
## Creating a Azure DNS zone
## Creating an Azure DNS zone
The Azure provider for ExternalDNS will find suitable zones for domains it manages; it will
not automatically create zones.
@ -34,69 +34,45 @@ If using your own domain that was registered with a third-party domain registrar
name servers to the values in the `nameServers` field from the JSON data returned by the `az network dns zone create` command.
Please consult your registrar's documentation on how to do that.
## Creating Azure Credentials Secret
The Azure DNS provider expects, by default, that the configuration file is at `/etc/kubernetes/azure.json`. This can be overridden with
the `--azure-config-file` option when starting ExternalDNS.
## Permissions to modify DNS zone
External-DNS needs permissions to make changes in the Azure DNS server. These permissions are defined in a Service Principal that should be made available to External-DNS as a configuration file.
### Use provisioned VM configuration file
When running within Azure (ACS or AKS), the agent and master VMs are already provisioned with the configuration file at `/etc/kubernetes/azure.json`.
The Azure DNS provider expects, by default, that the configuration file is at `/etc/kubernetes/azure.json`. This can be overridden with the `--azure-config-file` option when starting ExternalDNS.
If you want to use the file directly, make sure that the service principal that is given there has access to contribute to the resource group containing the Azure DNS zone(s).
To use the file, replace the directive
```yaml
volumes:
- name: azure-config-file
secret:
secretName: azure-config-file
```
with
```yaml
volumes:
- name: azure-config-file
hostPath:
path: /etc/kubernetes/azure.json
type: File
```
in the manifests below.
### Use custom configuration file
If you want to customize the configuration, for example because you want to use a different service principal, you have to manually create a secret.
This is also required if the Kubernetes cluster is not hosted in Azure Container Services (ACS or AKS) and you still want to use Azure DNS.
The secret should contain an object named azure.json with content similar to this:
### Creating configuration file
The preferred way to inject the configuration file is by using a Kubernetes secret. The secret should contain an object named azure.json with content similar to this:
```json
{
"tenantId": "01234abc-de56-ff78-abc1-234567890def",
"subscriptionId": "01234abc-de56-ff78-abc1-234567890def",
"aadClientId": "01234abc-de56-ff78-abc1-234567890def",
"aadClientSecret": "uKiuXeiwui4jo9quae9o",
"resourceGroup": "MyDnsResourceGroup",
"aadClientId": "01234abc-de56-ff78-abc1-234567890def",
"aadClientSecret": "uKiuXeiwui4jo9quae9o"
}
```
You can find the `tenantId` by running `az account show` or by selecting Azure Active Directory in the Azure Portal and checking the _Directory ID_ under Properties.
You can find the `tenantId` by running `az account show --query "tenantId"` or by selecting Azure Active Directory in the Azure Portal and checking the _Directory ID_ under Properties.
You can find the `subscriptionId` by running `az account show --query "id"` or by selecting Subscriptions in the Azure Portal.
To create the secret:
The `resourceGroup` is the Resource Group created in a previous step.
```
$ kubectl create secret generic azure-config-file --from-file=/local/path/to/azure.json
The `aadClientID` and `aaClientSecret` are assoiated with the Service Principal, that you need to create next.
### Creating service principal
A Service Principal with a minimum access level of `contributor` to the DNS zone(s) and `reader` to the resource group containing the Azure DNS zone(s) is necessary for ExternalDNS to be able to edit DNS records. However, other more permissive access levels will work too (e.g. `contributor` to the resource group or the whole subscription).
This is an Azure CLI example on how to query the Azure API for the information required for the Resource Group and DNS zone you would have already created in previous steps.
``` bash
> az login
```
#### Create service principal
A Service Principal with a minimum access level of contribute to the resource group containing the Azure DNS zone(s) is necessary for ExternalDNS to be able to edit DNS records. This is an Azure CLI example on how to query the Azure API for the information required for the Resource Group and DNS zone you would have already created in previous steps.
Find the relevant subscription and make sure it is selected (the same subscriptionId should be set into azure.json)
```
>az login
...
# find the relevant subscription and set the az context. id = subscriptionId value in the azure.json.
>az account list
``` bash
> az account list
{
"cloudName": "AzureCloud",
"id": "<subscriptionId GUID>",
@ -108,16 +84,15 @@ A Service Principal with a minimum access level of contribute to the resource gr
"name": "name",
"type": "user"
}
>az account set -s id
...
>az group show --name externaldns
{
"id": "/subscriptions/id/resourceGroups/externaldns",
...
}
# use the id from the previous step in the scopes argument
>az ad sp create-for-rbac --role="Contributor" --scopes="/subscriptions/id/resourceGroups/externaldns" -n ExternalDnsServicePrincipal
# select the subscription
> az account set -s <subscriptionId GUID>
...
```
Create the service principal
``` bash
> az ad sp create-for-rbac -n ExternalDnsServicePrincipal
{
"appId": "appId GUID", <-- aadClientId value
...
@ -126,7 +101,40 @@ A Service Principal with a minimum access level of contribute to the resource gr
}
```
#### Azure Managed Service Identity (MSI)
Assign the rights for the service principal
```
# find out the resource ids of the resource group where the dns zone is deployed, and the dns zone itself
> az group show --name externaldns
{
"id": "/subscriptions/id/resourceGroups/externaldns",
...
}
> az network dns zone show --name example.com -g externaldns
{
"id": "/subscriptions/.../resourceGroups/externaldns/providers/Microsoft.Network/dnszones/example.com",
...
}
```
```
# assign the rights to the created service principal, using the resource ids from previous step
# 1. as a reader to the resource group
> az role assignment create --role "Reader" --assignee <appId GUID> --scope <resource group resource id>
# 2. as a contributor to DNS Zone itself
> az role assignment create --role "Contributor" --assignee <appId GUID> --scope <dns zone resource id>
```
Now you can create a file named 'azure.json' with values gathered above and with the structure of the example above. Use this file to create a Kubernetes secret:
```
$ kubectl create secret generic azure-config-file --from-file=/local/path/to/azure.json
```
### Azure Managed Service Identity (MSI)
If [Azure Managed Service Identity (MSI)](https://docs.microsoft.com/en-us/azure/active-directory/managed-service-identity/overview) is enabled for virtual machines, then there is no need to create separate service principal.
@ -149,7 +157,6 @@ kubectl create secret generic azure-config-file --from-file=azure.json
```
## Deploy ExternalDNS
This deployment assumes that you will be using nginx-ingress. When using nginx-ingress do not deploy it as a Daemon Set. This causes nginx-ingress to write the Cluster IP of the backend pods in the ingress status.loadbalancer.ip property which then has external-dns write the Cluster IP(s) in DNS vs. the nginx-ingress service external IP.

View File

@ -16,7 +16,7 @@ Snippet from [Cloudflare - Getting Started](https://api.cloudflare.com/#getting-
>Cloudflare's API exposes the entire Cloudflare infrastructure via a standardized programmatic interface. Using Cloudflare's API, you can do just about anything you can do on cloudflare.com via the customer dashboard.
>The Cloudflare API is a RESTful API based on HTTPS requests and JSON responses. If you are registered with Cloudflare, you can obtain your API key from the bottom of the "My Account" page, found here: [Go to My account](https://www.cloudflare.com/a/account).
>The Cloudflare API is a RESTful API based on HTTPS requests and JSON responses. If you are registered with Cloudflare, you can obtain your API key from the bottom of the "My Account" page, found here: [Go to My account](https://dash.cloudflare.com/profile).
The environment vars `CF_API_KEY` and `CF_API_EMAIL` will be needed to run ExternalDNS with Cloudflare.
@ -193,8 +193,8 @@ This should show the external IP address of the service as the A record for your
Now that we have verified that ExternalDNS will automatically manage Cloudflare DNS records, we can delete the tutorial's example:
```
$ kubectl delete service -f nginx.yaml
$ kubectl delete service -f externaldns.yaml
$ kubectl delete -f nginx.yaml
$ kubectl delete -f externaldns.yaml
```
## Setting cloudflare-proxied on a per-ingress basis

235
docs/tutorials/coredns.md Normal file
View File

@ -0,0 +1,235 @@
# Setting up ExternalDNS for CoreDNS with minikube
This tutorial describes how to setup ExternalDNS for usage within a [minikube](https://github.com/kubernetes/minikube) cluster that makes use of [CoreDNS](https://github.com/coredns/coredns) and [nginx ingress controller](https://github.com/kubernetes/ingress-nginx).
You need to:
* install CoreDNS with [etcd](https://github.com/etcd-io/etcd) enabled
* install external-dns with coredns as a provider
* enable ingress controller for the minikube cluster
## Creating a cluster
```
minikube start
```
## Installing CoreDNS with etcd enabled
Helm chart is used to install etcd and CoreDNS.
### Initializing helm chart
```
helm init
```
### Installing etcd
[etcd operator](https://github.com/coreos/etcd-operator) is used to manage etcd clusters.
```
helm install stable/etcd-operator --name my-etcd-op
```
etcd cluster is installed with example yaml from etcd operator website.
```
kubectl apply -f https://raw.githubusercontent.com/coreos/etcd-operator/master/example/example-etcd-cluster.yaml
```
### Installing CoreDNS
In order to make CoreDNS work with etcd backend, values.yaml of the chart should be changed with corresponding configurations.
```
wget https://raw.githubusercontent.com/helm/charts/master/stable/coredns/values.yaml
```
You need to edit/patch the file with below diff
```
diff --git a/values.yaml b/values.yaml
index 964e72b..e2fa934 100644
--- a/values.yaml
+++ b/values.yaml
@@ -27,12 +27,12 @@ service:
rbac:
# If true, create & use RBAC resources
- create: false
+ create: true
# Ignored if rbac.create is true
serviceAccountName: default
# isClusterService specifies whether chart should be deployed as cluster-service or normal k8s app.
-isClusterService: true
+isClusterService: false
servers:
- zones:
@@ -51,6 +51,12 @@ servers:
parameters: 0.0.0.0:9153
- name: proxy
parameters: . /etc/resolv.conf
+ - name: etcd
+ parameters: example.org
+ configBlock: |-
+ stubzones
+ path /skydns
+ endpoint http://10.105.68.165:2379
# Complete example with all the options:
# - zones: # the `zones` block can be left out entirely, defaults to "."
```
**Note**:
* IP address of etcd's endpoint should be get from etcd client service. It should be "example-etcd-cluster-client" in this example. This IP address is used through this document for etcd endpoint configuration.
```
$ kubectl get svc example-etcd-cluster-client
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
example-etcd-cluster-client ClusterIP 10.105.68.165 <none> 2379/TCP 16m
```
* Parameters should configure your own domain. "example.org" is used in this example.
After configuration done in values.yaml, you can install coredns chart.
```
helm install --name my-coredns --values values.yaml stable/coredns
```
## Installing ExternalDNS
### Install external ExternalDNS
ETCD_URLS is configured to etcd client service address.
#### Manifest (for clusters without RBAC enabled)
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: external-dns
namespace: kube-system
spec:
strategy:
type: Recreate
selector:
matchLabels:
app: external-dns
template:
metadata:
labels:
app: external-dns
spec:
containers:
- name: external-dns
image: registry.opensource.zalan.do/teapot/external-dns:latest
args:
- --source=ingress
- --provider=coredns
- --log-level=debug # debug only
env:
- name: ETCD_URLS
value: http://10.105.68.165:2379
```
#### Manifest (for clusters with RBAC enabled)
```yaml
---
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: kube-system
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-dns
namespace: kube-system
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: external-dns
namespace: kube-system
spec:
strategy:
type: Recreate
selector:
matchLabels:
app: external-dns
template:
metadata:
labels:
app: external-dns
spec:
serviceAccountName: external-dns
containers:
- name: external-dns
image: registry.opensource.zalan.do/teapot/external-dns:latest
args:
- --source=ingress
- --provider=coredns
- --log-level=debug # debug only
env:
- name: ETCD_URLS
value: http://10.105.68.165:2379
```
## Enable the ingress controller
You can use the ingress controller in minikube cluster. It needs to enable ingress addon in the cluster.
```
minikube addons enable ingress
```
## Testing ingress example
```
$ cat ingress.yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: nginx
annotations:
kubernetes.io/ingress.class: "nginx"
spec:
rules:
- host: nginx.example.org
http:
paths:
- backend:
serviceName: nginx
servicePort: 80
$ kubectl apply -f ingress.yaml
ingress.extensions "nginx" created
```
Wait a moment until DNS has the ingress IP. The DNS service IP is from CoreDNS service. It is "my-coredns-coredns" in this example.
```
$ kubectl get svc my-coredns-coredns
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
my-coredns-coredns ClusterIP 10.100.4.143 <none> 53/UDP 12m
$ kubectl get ingress
NAME HOSTS ADDRESS PORTS AGE
nginx nginx.example.org 10.0.2.15 80 2m
$ kubectl run -it --rm --restart=Never --image=infoblox/dnstools:latest dnstools
If you don't see a command prompt, try pressing enter.
dnstools# dig @10.100.4.143 nginx.example.org +short
10.0.2.15
dnstools#
```

View File

@ -12,6 +12,39 @@ key "externaldns-key" {
```
- `Warning!` Bind server configuration should enable for this key AFXR zone transfer protocol. It is used for listing DNS records.
```text
# cat /etc/named.conf
...
include "/etc/rndc.key";
controls {
inet 123.123.123.123 port 953 allow { 10.x.y.151; } keys { "externaldns-key"; };
};
options {
include "/etc/named/options.conf";
};
include "/etc/named/zones.conf";
...
# cat /etc/named/options.conf
...
dnssec-enable yes;
dnssec-validation yes;
...
# cat /etc/named/zones.conf
...
zone "example.com" {
type master;
file "/var/named/dynamic/db.example.com";
update-policy {
grant externaldns-key zonesub ANY;
};
};
...
```
## RFC2136 provider configuration:
- Example fragment of real configuration of ExternalDNS service pod.

View File

@ -99,6 +99,7 @@ func main() {
domainFilter := provider.NewDomainFilter(cfg.DomainFilter)
zoneIDFilter := provider.NewZoneIDFilter(cfg.ZoneIDFilter)
zoneTypeFilter := provider.NewZoneTypeFilter(cfg.AWSZoneType)
zoneTagFilter := provider.NewZoneTagFilter(cfg.AWSZoneTagFilter)
var p provider.Provider
switch cfg.Provider {
@ -110,10 +111,12 @@ func main() {
DomainFilter: domainFilter,
ZoneIDFilter: zoneIDFilter,
ZoneTypeFilter: zoneTypeFilter,
ZoneTagFilter: zoneTagFilter,
BatchChangeSize: cfg.AWSBatchChangeSize,
BatchChangeInterval: cfg.AWSBatchChangeInterval,
EvaluateTargetHealth: cfg.AWSEvaluateTargetHealth,
AssumeRole: cfg.AWSAssumeRole,
APIRetries: cfg.AWSAPIRetries,
DryRun: cfg.DryRun,
},
)

View File

@ -56,10 +56,12 @@ type Config struct {
AlibabaCloudConfigFile string
AlibabaCloudZoneType string
AWSZoneType string
AWSZoneTagFilter []string
AWSAssumeRole string
AWSBatchChangeSize int
AWSBatchChangeInterval time.Duration
AWSEvaluateTargetHealth bool
AWSAPIRetries int
AzureConfigFile string
AzureResourceGroup string
CloudflareProxied bool
@ -127,10 +129,12 @@ var defaultConfig = &Config{
DomainFilter: []string{},
AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json",
AWSZoneType: "",
AWSZoneTagFilter: []string{},
AWSAssumeRole: "",
AWSBatchChangeSize: 4000,
AWSBatchChangeSize: 1000,
AWSBatchChangeInterval: time.Second,
AWSEvaluateTargetHealth: true,
AWSAPIRetries: 3,
AzureConfigFile: "/etc/kubernetes/azure.json",
AzureResourceGroup: "",
CloudflareProxied: false,
@ -162,7 +166,7 @@ var defaultConfig = &Config{
ExoscaleEndpoint: "https://api.exoscale.ch/dns",
ExoscaleAPIKey: "",
ExoscaleAPISecret: "",
CRDSourceAPIVersion: "externaldns.k8s.io/v1alpha",
CRDSourceAPIVersion: "externaldns.k8s.io/v1alpha1",
CRDSourceKind: "DNSEndpoint",
ServiceTypeFilter: []string{},
RFC2136Host: "",
@ -241,10 +245,12 @@ func (cfg *Config) ParseFlags(args []string) error {
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)
app.Flag("alibaba-cloud-zone-type", "When using the Alibaba Cloud provider, filter for zones of this type (optional, options: public, private)").Default(defaultConfig.AlibabaCloudZoneType).EnumVar(&cfg.AlibabaCloudZoneType, "", "public", "private")
app.Flag("aws-zone-type", "When using the AWS provider, filter for zones of this type (optional, options: public, private)").Default(defaultConfig.AWSZoneType).EnumVar(&cfg.AWSZoneType, "", "public", "private")
app.Flag("aws-zone-tags", "When using the AWS provider, filter for zones with these tags").Default("").StringsVar(&cfg.AWSZoneTagFilter)
app.Flag("aws-assume-role", "When using the AWS provider, assume this IAM role. Useful for hosted zones in another AWS account. Specify the full ARN, e.g. `arn:aws:iam::123455567:role/external-dns` (optional)").Default(defaultConfig.AWSAssumeRole).StringVar(&cfg.AWSAssumeRole)
app.Flag("aws-batch-change-size", "When using the AWS provider, set the maximum number of changes that will be applied in each batch.").Default(strconv.Itoa(defaultConfig.AWSBatchChangeSize)).IntVar(&cfg.AWSBatchChangeSize)
app.Flag("aws-batch-change-interval", "When using the AWS provider, set the interval between batch changes.").Default(defaultConfig.AWSBatchChangeInterval.String()).DurationVar(&cfg.AWSBatchChangeInterval)
app.Flag("aws-evaluate-target-health", "When using the AWS provider, set whether to evaluate the health of a DNS target (default: enabled, disable with --no-aws-evaluate-target-health)").Default(strconv.FormatBool(defaultConfig.AWSEvaluateTargetHealth)).BoolVar(&cfg.AWSEvaluateTargetHealth)
app.Flag("aws-api-retries", "When using the AWS provider, set the maximum number of retries for API calls before giving up.").Default(strconv.Itoa(defaultConfig.AWSAPIRetries)).IntVar(&cfg.AWSAPIRetries)
app.Flag("azure-config-file", "When using the Azure provider, specify the Azure configuration file (required when --provider=azure").Default(defaultConfig.AzureConfigFile).StringVar(&cfg.AzureConfigFile)
app.Flag("azure-resource-group", "When using the Azure provider, override the Azure resource group to use (optional)").Default(defaultConfig.AzureResourceGroup).StringVar(&cfg.AzureResourceGroup)
app.Flag("cloudflare-proxied", "When using the Cloudflare provider, specify if the proxy mode must be enabled (default: disabled)").BoolVar(&cfg.CloudflareProxied)

View File

@ -43,10 +43,12 @@ var (
ZoneIDFilter: []string{""},
AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json",
AWSZoneType: "",
AWSZoneTagFilter: []string{""},
AWSAssumeRole: "",
AWSBatchChangeSize: 4000,
AWSBatchChangeSize: 1000,
AWSBatchChangeInterval: time.Second,
AWSEvaluateTargetHealth: true,
AWSAPIRetries: 3,
AzureConfigFile: "/etc/kubernetes/azure.json",
AzureResourceGroup: "",
CloudflareProxied: false,
@ -75,7 +77,7 @@ var (
ExoscaleEndpoint: "https://api.exoscale.ch/dns",
ExoscaleAPIKey: "",
ExoscaleAPISecret: "",
CRDSourceAPIVersion: "externaldns.k8s.io/v1alpha",
CRDSourceAPIVersion: "externaldns.k8s.io/v1alpha1",
CRDSourceKind: "DNSEndpoint",
}
@ -94,10 +96,12 @@ var (
ZoneIDFilter: []string{"/hostedzone/ZTST1", "/hostedzone/ZTST2"},
AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json",
AWSZoneType: "private",
AWSZoneTagFilter: []string{"tag=foo"},
AWSAssumeRole: "some-other-role",
AWSBatchChangeSize: 100,
AWSBatchChangeInterval: time.Second * 2,
AWSEvaluateTargetHealth: false,
AWSAPIRetries: 13,
AzureConfigFile: "azure.json",
AzureResourceGroup: "arg",
CloudflareProxied: true,
@ -189,9 +193,11 @@ func TestParseFlags(t *testing.T) {
"--zone-id-filter=/hostedzone/ZTST1",
"--zone-id-filter=/hostedzone/ZTST2",
"--aws-zone-type=private",
"--aws-zone-tags=tag=foo",
"--aws-assume-role=some-other-role",
"--aws-batch-change-size=100",
"--aws-batch-change-interval=2s",
"--aws-api-retries=13",
"--no-aws-evaluate-target-health",
"--policy=upsert-only",
"--registry=noop",
@ -248,10 +254,12 @@ func TestParseFlags(t *testing.T) {
"EXTERNAL_DNS_TLS_CLIENT_CERT_KEY": "/path/to/key.pem",
"EXTERNAL_DNS_ZONE_ID_FILTER": "/hostedzone/ZTST1\n/hostedzone/ZTST2",
"EXTERNAL_DNS_AWS_ZONE_TYPE": "private",
"EXTERNAL_DNS_AWS_ZONE_TAGS": "tag=foo",
"EXTERNAL_DNS_AWS_ASSUME_ROLE": "some-other-role",
"EXTERNAL_DNS_AWS_BATCH_CHANGE_SIZE": "100",
"EXTERNAL_DNS_AWS_BATCH_CHANGE_INTERVAL": "2s",
"EXTERNAL_DNS_AWS_EVALUATE_TARGET_HEALTH": "0",
"EXTERNAL_DNS_AWS_API_RETRIES": "13",
"EXTERNAL_DNS_POLICY": "upsert-only",
"EXTERNAL_DNS_REGISTRY": "noop",
"EXTERNAL_DNS_TXT_OWNER_ID": "owner-1",

View File

@ -17,6 +17,7 @@ limitations under the License.
package plan
import (
"fmt"
"strings"
"github.com/google/go-cmp/cmp"
@ -79,8 +80,12 @@ type planTableRow struct {
candidates []*endpoint.Endpoint
}
func (t planTableRow) String() string {
return fmt.Sprintf("planTableRow{current=%v, candidates=%v}", t.current, t.candidates)
}
func (t planTable) addCurrent(e *endpoint.Endpoint) {
dnsName := sanitizeDNSName(e.DNSName)
dnsName := normalizeDNSName(e.DNSName)
if _, ok := t.rows[dnsName]; !ok {
t.rows[dnsName] = &planTableRow{}
}
@ -88,7 +93,7 @@ func (t planTable) addCurrent(e *endpoint.Endpoint) {
}
func (t planTable) addCandidate(e *endpoint.Endpoint) {
dnsName := sanitizeDNSName(e.DNSName)
dnsName := normalizeDNSName(e.DNSName)
if _, ok := t.rows[dnsName]; !ok {
t.rows[dnsName] = &planTableRow{}
}
@ -209,8 +214,12 @@ func filterRecordsForPlan(records []*endpoint.Endpoint) []*endpoint.Endpoint {
return filtered
}
// sanitizeDNSName checks if the DNS name is correct
// for now it only removes space and lower case
func sanitizeDNSName(dnsName string) string {
return strings.TrimSpace(strings.ToLower(dnsName))
// normalizeDNSName converts a DNS name to a canonical form, so that we can use string equality
// it: removes space, converts to lower case, ensures there is a trailing dot
func normalizeDNSName(dnsName string) string {
s := strings.TrimSpace(strings.ToLower(dnsName))
if !strings.HasSuffix(s, ".") {
s += "."
}
return s
}

View File

@ -438,54 +438,58 @@ func validateEntries(t *testing.T, entries, expected []*endpoint.Endpoint) {
}
}
func TestSanitizeDNSName(t *testing.T) {
func TestNormalizeDNSName(t *testing.T) {
records := []struct {
dnsName string
expect string
}{
{
"3AAAA.FOO.BAR.COM ",
"3aaaa.foo.bar.com",
"3aaaa.foo.bar.com.",
},
{
" example.foo.com",
"example.foo.com",
" example.foo.com.",
"example.foo.com.",
},
{
"example123.foo.com ",
"example123.foo.com",
"example123.foo.com.",
},
{
"foo",
"foo",
"foo.",
},
{
"123foo.bar",
"123foo.bar",
"123foo.bar.",
},
{
"foo.com",
"foo.com",
"foo.com.",
},
{
"foo.com.",
"foo.com.",
},
{
"foo123.COM",
"foo123.com",
"foo123.com.",
},
{
"my-exaMple3.FOO.BAR.COM",
"my-example3.foo.bar.com",
"my-example3.foo.bar.com.",
},
{
" my-example1214.FOO-1235.BAR-foo.COM ",
"my-example1214.foo-1235.bar-foo.com",
"my-example1214.foo-1235.bar-foo.com.",
},
{
"my-example-my-example-1214.FOO-1235.BAR-foo.COM",
"my-example-my-example-1214.foo-1235.bar-foo.com",
"my-example-my-example-1214.foo-1235.bar-foo.com.",
},
}
for _, r := range records {
gotName := sanitizeDNSName(r.dnsName)
gotName := normalizeDNSName(r.dnsName)
assert.Equal(t, r.expect, gotName)
}
}

View File

@ -42,6 +42,7 @@ const (
defaultAlibabaCloudPrivateZoneRecordTTL = 60
defaultAlibabaCloudPageSize = 50
nullHostAlibabaCloud = "@"
pVTZDoamin = "pvtz.aliyuncs.com"
)
// AlibabaCloudDNSAPI is a minimal implementation of DNS API that we actually use, used primarily for unit testing.
@ -708,6 +709,7 @@ func (p *AlibabaCloudProvider) splitDNSName(endpoint *endpoint.Endpoint) (rr str
func (p *AlibabaCloudProvider) matchVPC(zoneID string) bool {
request := pvtz.CreateDescribeZoneInfoRequest()
request.ZoneId = zoneID
request.Domain = pVTZDoamin
response, err := p.getPvtzClient().DescribeZoneInfo(request)
if err != nil {
log.Errorf("Failed to describe zone info %s in Alibaba Cloud DNS: %v", zoneID, err)
@ -730,7 +732,7 @@ func (p *AlibabaCloudProvider) privateZones() ([]pvtz.Zone, error) {
request := pvtz.CreateDescribeZonesRequest()
request.PageSize = requests.NewInteger(defaultAlibabaCloudPageSize)
request.PageNumber = "1"
request.Domain = pVTZDoamin
for {
response, err := p.getPvtzClient().DescribeZones(request)
if err != nil {
@ -738,7 +740,7 @@ func (p *AlibabaCloudProvider) privateZones() ([]pvtz.Zone, error) {
return nil, err
}
for _, zone := range response.Zones.Zone {
log.Debugf("Zone: %++v", zone)
log.Infof("PrivateZones zone: %++v", zone)
if !p.zoneIDFilter.Match(zone.ZoneId) {
continue
@ -784,6 +786,7 @@ func (p *AlibabaCloudProvider) getPrivateZones() (map[string]*alibabaPrivateZone
request.ZoneId = zone.ZoneId
request.PageSize = requests.NewInteger(defaultAlibabaCloudPageSize)
request.PageNumber = "1"
request.Domain = pVTZDoamin
var records []pvtz.Record
for {
@ -884,6 +887,7 @@ func (p *AlibabaCloudProvider) createPrivateZoneRecord(zones map[string]*alibaba
request.ZoneId = zone.ZoneId
request.Type = endpoint.RecordType
request.Rr = rr
request.Domain = pVTZDoamin
ttl := int(endpoint.RecordTTL)
if ttl != 0 {
@ -927,6 +931,7 @@ func (p *AlibabaCloudProvider) deletePrivateZoneRecord(recordID int) error {
request := pvtz.CreateDeleteZoneRecordRequest()
request.RecordId = requests.NewInteger(recordID)
request.Domain = pVTZDoamin
response, err := p.getPvtzClient().DeleteZoneRecord(request)
if err == nil {
@ -998,6 +1003,7 @@ func (p *AlibabaCloudProvider) updatePrivateZoneRecord(record pvtz.Record, endpo
request.Rr = record.Rr
request.Type = record.Type
request.Value = record.Value
request.Domain = pVTZDoamin
ttl := int(endpoint.RecordTTL)
if ttl != 0 {
request.Ttl = requests.NewInteger(ttl)

View File

@ -58,6 +58,7 @@ var (
"eu-west-1.elb.amazonaws.com": "Z32O12XQLNTSW2",
"eu-west-2.elb.amazonaws.com": "ZHURV8PSTC4K8",
"eu-west-3.elb.amazonaws.com": "Z3Q77PNBQS71R4",
"eu-north-1.elb.amazonaws.com": "Z23TAZ6LKFMNIO",
"sa-east-1.elb.amazonaws.com": "Z2P70J7HTTTPLU",
// Network Load Balancers
"elb.us-east-2.amazonaws.com": "ZLMOA37VPKANP",
@ -74,6 +75,7 @@ var (
"elb.eu-west-1.amazonaws.com": "Z2IFOLAFXWLO4F",
"elb.eu-west-2.amazonaws.com": "ZD4D7Y8KGAS4G",
"elb.eu-west-3.amazonaws.com": "Z1CMS0P5QUZ6D5",
"elb.eu-north-1.amazonaws.com": "Z1UDT6IFJ4EJM",
"elb.sa-east-1.amazonaws.com": "ZTK26PT1VY4CU",
}
)
@ -85,6 +87,7 @@ type Route53API interface {
ChangeResourceRecordSets(*route53.ChangeResourceRecordSetsInput) (*route53.ChangeResourceRecordSetsOutput, error)
CreateHostedZone(*route53.CreateHostedZoneInput) (*route53.CreateHostedZoneOutput, error)
ListHostedZonesPages(input *route53.ListHostedZonesInput, fn func(resp *route53.ListHostedZonesOutput, lastPage bool) (shouldContinue bool)) error
ListTagsForResource(input *route53.ListTagsForResourceInput) (*route53.ListTagsForResourceOutput, error)
}
// AWSProvider is an implementation of Provider for AWS Route53.
@ -100,6 +103,8 @@ type AWSProvider struct {
zoneIDFilter ZoneIDFilter
// filter hosted zones by type (e.g. private or public)
zoneTypeFilter ZoneTypeFilter
// filter hosted zones by tags
zoneTagFilter ZoneTagFilter
}
// AWSConfig contains configuration to create a new AWS provider.
@ -107,16 +112,18 @@ type AWSConfig struct {
DomainFilter DomainFilter
ZoneIDFilter ZoneIDFilter
ZoneTypeFilter ZoneTypeFilter
ZoneTagFilter ZoneTagFilter
BatchChangeSize int
BatchChangeInterval time.Duration
EvaluateTargetHealth bool
AssumeRole string
APIRetries int
DryRun bool
}
// NewAWSProvider initializes a new AWS Route53 based Provider.
func NewAWSProvider(awsConfig AWSConfig) (*AWSProvider, error) {
config := aws.NewConfig()
config := aws.NewConfig().WithMaxRetries(awsConfig.APIRetries)
config.WithHTTPClient(
instrumented_http.NewClient(config.HTTPClient, &instrumented_http.Callbacks{
@ -145,6 +152,7 @@ func NewAWSProvider(awsConfig AWSConfig) (*AWSProvider, error) {
domainFilter: awsConfig.DomainFilter,
zoneIDFilter: awsConfig.ZoneIDFilter,
zoneTypeFilter: awsConfig.ZoneTypeFilter,
zoneTagFilter: awsConfig.ZoneTagFilter,
batchChangeSize: awsConfig.BatchChangeSize,
batchChangeInterval: awsConfig.BatchChangeInterval,
evaluateTargetHealth: awsConfig.EvaluateTargetHealth,
@ -158,6 +166,7 @@ func NewAWSProvider(awsConfig AWSConfig) (*AWSProvider, error) {
func (p *AWSProvider) Zones() (map[string]*route53.HostedZone, error) {
zones := make(map[string]*route53.HostedZone)
var tagErr error
f := func(resp *route53.ListHostedZonesOutput, lastPage bool) (shouldContinue bool) {
for _, zone := range resp.HostedZones {
if !p.zoneIDFilter.Match(aws.StringValue(zone.Id)) {
@ -172,6 +181,18 @@ func (p *AWSProvider) Zones() (map[string]*route53.HostedZone, error) {
continue
}
// Only fetch tags if a tag filter was specified
if !p.zoneTagFilter.IsEmpty() {
tags, err := p.tagsForZone(*zone.Id)
if err != nil {
tagErr = err
return false
}
if !p.zoneTagFilter.Match(tags) {
continue
}
}
zones[aws.StringValue(zone.Id)] = zone
}
@ -182,6 +203,9 @@ func (p *AWSProvider) Zones() (map[string]*route53.HostedZone, error) {
if err != nil {
return nil, err
}
if tagErr != nil {
return nil, tagErr
}
for _, zone := range zones {
log.Debugf("Considering zone: %s (domain: %s)", aws.StringValue(zone.Id), aws.StringValue(zone.Name))
@ -412,6 +436,21 @@ func (p *AWSProvider) newChange(action string, endpoint *endpoint.Endpoint) *rou
return change
}
func (p *AWSProvider) tagsForZone(zoneID string) (map[string]string, error) {
response, err := p.client.ListTagsForResource(&route53.ListTagsForResourceInput{
ResourceType: aws.String("hostedzone"),
ResourceId: aws.String(zoneID),
})
if err != nil {
return nil, err
}
tagMap := map[string]string{}
for _, tag := range response.ResourceTagSet.Tags {
tagMap[*tag.Key] = *tag.Value
}
return tagMap, nil
}
func batchChangeSet(cs []*route53.Change, batchSize int) [][]*route53.Change {
if len(cs) <= batchSize {
return [][]*route53.Change{cs}

View File

@ -50,6 +50,7 @@ var _ Route53API = &Route53APIStub{}
type Route53APIStub struct {
zones map[string]*route53.HostedZone
recordSets map[string]map[string][]*route53.ResourceRecordSet
zoneTags map[string][]*route53.Tag
m dynamicMock
}
@ -66,6 +67,7 @@ func NewRoute53APIStub() *Route53APIStub {
return &Route53APIStub{
zones: make(map[string]*route53.HostedZone),
recordSets: make(map[string]map[string][]*route53.ResourceRecordSet),
zoneTags: make(map[string][]*route53.Tag),
}
}
@ -95,6 +97,20 @@ func wildcardEscape(s string) string {
return s
}
func (r *Route53APIStub) ListTagsForResource(input *route53.ListTagsForResourceInput) (*route53.ListTagsForResourceOutput, error) {
if aws.StringValue(input.ResourceType) == "hostedzone" {
tags := r.zoneTags[aws.StringValue(input.ResourceId)]
return &route53.ListTagsForResourceOutput{
ResourceTagSet: &route53.ResourceTagSet{
ResourceId: input.ResourceId,
ResourceType: input.ResourceType,
Tags: tags,
},
}, nil
}
return &route53.ListTagsForResourceOutput{}, nil
}
func (r *Route53APIStub) ChangeResourceRecordSets(input *route53.ChangeResourceRecordSetsInput) (*route53.ChangeResourceRecordSetsOutput, error) {
if r.m.isMocked("ChangeResourceRecordSets", input) {
return r.m.ChangeResourceRecordSets(input)
@ -231,15 +247,17 @@ func TestAWSZones(t *testing.T) {
msg string
zoneIDFilter ZoneIDFilter
zoneTypeFilter ZoneTypeFilter
zoneTagFilter ZoneTagFilter
expectedZones map[string]*route53.HostedZone
}{
{"no filter", NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), allZones},
{"public filter", NewZoneIDFilter([]string{}), NewZoneTypeFilter("public"), publicZones},
{"private filter", NewZoneIDFilter([]string{}), NewZoneTypeFilter("private"), privateZones},
{"unknown filter", NewZoneIDFilter([]string{}), NewZoneTypeFilter("unknown"), noZones},
{"zone id filter", NewZoneIDFilter([]string{"/hostedzone/zone-3.ext-dns-test-2.teapot.zalan.do."}), NewZoneTypeFilter(""), privateZones},
{"no filter", NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), NewZoneTagFilter([]string{}), allZones},
{"public filter", NewZoneIDFilter([]string{}), NewZoneTypeFilter("public"), NewZoneTagFilter([]string{}), publicZones},
{"private filter", NewZoneIDFilter([]string{}), NewZoneTypeFilter("private"), NewZoneTagFilter([]string{}), privateZones},
{"unknown filter", NewZoneIDFilter([]string{}), NewZoneTypeFilter("unknown"), NewZoneTagFilter([]string{}), noZones},
{"zone id filter", NewZoneIDFilter([]string{"/hostedzone/zone-3.ext-dns-test-2.teapot.zalan.do."}), NewZoneTypeFilter(""), NewZoneTagFilter([]string{}), privateZones},
{"tag filter", NewZoneIDFilter([]string{}), NewZoneTypeFilter(""), NewZoneTagFilter([]string{"zone=3"}), privateZones},
} {
provider, _ := newAWSProvider(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), ti.zoneIDFilter, ti.zoneTypeFilter, defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{})
provider, _ := newAWSProviderWithTagFilter(t, NewDomainFilter([]string{"ext-dns-test-2.teapot.zalan.do."}), ti.zoneIDFilter, ti.zoneTypeFilter, ti.zoneTagFilter, defaultEvaluateTargetHealth, false, []*endpoint.Endpoint{})
zones, err := provider.Zones()
require.NoError(t, err)
@ -1035,8 +1053,11 @@ func escapeAWSRecords(t *testing.T, provider *AWSProvider, zone string) {
require.NoError(t, err)
}
}
func newAWSProvider(t *testing.T, domainFilter DomainFilter, zoneIDFilter ZoneIDFilter, zoneTypeFilter ZoneTypeFilter, evaluateTargetHealth, dryRun bool, records []*endpoint.Endpoint) (*AWSProvider, *Route53APIStub) {
return newAWSProviderWithTagFilter(t, domainFilter, zoneIDFilter, zoneTypeFilter, NewZoneTagFilter([]string{}), evaluateTargetHealth, dryRun, records)
}
func newAWSProviderWithTagFilter(t *testing.T, domainFilter DomainFilter, zoneIDFilter ZoneIDFilter, zoneTypeFilter ZoneTypeFilter, zoneTagFilter ZoneTagFilter, evaluateTargetHealth, dryRun bool, records []*endpoint.Endpoint) (*AWSProvider, *Route53APIStub) {
client := NewRoute53APIStub()
provider := &AWSProvider{
@ -1047,6 +1068,7 @@ func newAWSProvider(t *testing.T, domainFilter DomainFilter, zoneIDFilter ZoneID
domainFilter: domainFilter,
zoneIDFilter: zoneIDFilter,
zoneTypeFilter: zoneTypeFilter,
zoneTagFilter: zoneTagFilter,
dryRun: false,
}
@ -1075,6 +1097,8 @@ func newAWSProvider(t *testing.T, domainFilter DomainFilter, zoneIDFilter ZoneID
Config: &route53.HostedZoneConfig{PrivateZone: aws.Bool(false)},
})
setupZoneTags(provider.client.(*Route53APIStub))
setupAWSRecords(t, provider, records)
provider.dryRun = dryRun
@ -1082,6 +1106,40 @@ func newAWSProvider(t *testing.T, domainFilter DomainFilter, zoneIDFilter ZoneID
return provider, client
}
func setupZoneTags(client *Route53APIStub) {
addZoneTags(client.zoneTags, "/hostedzone/zone-1.ext-dns-test-2.teapot.zalan.do.", map[string]string{
"zone-1-tag-1": "tag-1-value",
"domain": "test-2",
"zone": "1",
})
addZoneTags(client.zoneTags, "/hostedzone/zone-2.ext-dns-test-2.teapot.zalan.do.", map[string]string{
"zone-2-tag-1": "tag-1-value",
"domain": "test-2",
"zone": "2",
})
addZoneTags(client.zoneTags, "/hostedzone/zone-3.ext-dns-test-2.teapot.zalan.do.", map[string]string{
"zone-3-tag-1": "tag-1-value",
"domain": "test-2",
"zone": "3",
})
addZoneTags(client.zoneTags, "/hostedzone/zone-4.ext-dns-test-2.teapot.zalan.do.", map[string]string{
"zone-4-tag-1": "tag-1-value",
"domain": "test-3",
"zone": "4",
})
}
func addZoneTags(tagMap map[string][]*route53.Tag, zoneID string, tags map[string]string) {
tagList := make([]*route53.Tag, 0, len(tags))
for k, v := range tags {
tagList = append(tagList, &route53.Tag{
Key: aws.String(k),
Value: aws.String(v),
})
}
tagMap[zoneID] = tagList
}
func validateRecords(t *testing.T, records []*route53.ResourceRecordSet, expected []*route53.ResourceRecordSet) {
assert.Equal(t, expected, records)
}

View File

@ -153,7 +153,7 @@ func (c etcdClient) DeleteService(key string) error {
ctx, cancel := context.WithTimeout(c.ctx, etcdTimeout)
defer cancel()
_, err := c.client.Delete(ctx, key)
_, err := c.client.Delete(ctx, key, etcdcv3.WithPrefix())
return err
}
@ -317,22 +317,26 @@ func (p coreDNSProvider) ApplyChanges(changes *plan.Changes) error {
if ep.RecordType == endpoint.RecordTypeTXT {
continue
}
prefix := ep.Labels[randomPrefixLabel]
if prefix == "" {
prefix = fmt.Sprintf("%08x", rand.Int31())
for _, target := range ep.Targets {
prefix := ep.Labels[randomPrefixLabel]
if prefix == "" {
prefix = fmt.Sprintf("%08x", rand.Int31())
}
service := Service{
Host: target,
Text: ep.Labels["originalText"],
Key: etcdKeyFor(prefix + "." + dnsName),
TargetStrip: strings.Count(prefix, ".") + 1,
TTL: uint32(ep.RecordTTL),
}
services = append(services, service)
}
service := Service{
Host: ep.Targets[0],
Text: ep.Labels["originalText"],
Key: etcdKeyFor(prefix + "." + dnsName),
TargetStrip: strings.Count(prefix, ".") + 1,
TTL: uint32(ep.RecordTTL),
}
services = append(services, service)
}
index := 0
for _, ep := range group {
if ep.RecordType != "TXT" {
if ep.RecordType != endpoint.RecordTypeTXT {
continue
}
if index >= len(services) {

View File

@ -26,6 +26,7 @@ import (
log "github.com/sirupsen/logrus"
"github.com/nesv/go-dynect/dynect"
"github.com/sanyu/dynectsoap/dynectsoap"
"github.com/kubernetes-incubator/external-dns/endpoint"
"github.com/kubernetes-incubator/external-dns/plan"
@ -34,9 +35,6 @@ import (
const (
// 10 minutes default timeout if not configured using flags
dynDefaultTTL = 600
// can store 20000 entries globally, that's about 4MB of memory
// may be made configurable in the future but 20K records seems like enough for a few zones
cacheMaxSize = 20000
// when rate limit is hit retry up to 5 times after sleep 1m between retries
dynMaxRetriesOnErrRateLimited = 5
@ -51,50 +49,10 @@ const (
restAPIPrefix = "/REST/"
)
// A simple non-thread-safe cache with TTL. The TTL of the records is used here to
// This cache is used to save on requests to DynAPI
type cache struct {
contents map[string]*entry
}
type entry struct {
expires int64
ep *endpoint.Endpoint
}
func (c *cache) Put(link string, ep *endpoint.Endpoint) {
// flush the whole cache on overflow
if len(c.contents) >= cacheMaxSize {
log.Debugf("Flushing cache")
c.contents = make(map[string]*entry)
}
c.contents[link] = &entry{
ep: ep,
expires: unixNow() + int64(ep.RecordTTL),
}
}
func unixNow() int64 {
return int64(time.Now().Unix())
}
func (c *cache) Get(link string) *endpoint.Endpoint {
result, ok := c.contents[link]
if !ok {
return nil
}
now := unixNow()
if result.expires < now {
delete(c.contents, link)
return nil
}
return result.ep
}
// DynConfig hold connection parameters to dyn.com and internal state
type DynConfig struct {
DomainFilter DomainFilter
@ -145,7 +103,6 @@ func (snap *ZoneSnapshot) StoreRecordsForSerial(zone string, serial int, records
// DynProvider is the actual interface impl.
type dynProviderState struct {
DynConfig
Cache *cache
LastLoginErrorTime int64
ZoneSnapshot *ZoneSnapshot
@ -186,9 +143,6 @@ type ZonePublishResponse struct {
func NewDynProvider(config DynConfig) (Provider, error) {
return &dynProviderState{
DynConfig: config,
Cache: &cache{
contents: make(map[string]*entry),
},
ZoneSnapshot: &ZoneSnapshot{
endpoints: map[string][]*endpoint.Endpoint{},
serials: map[string]int{},
@ -277,27 +231,6 @@ func merge(updateOld, updateNew []*endpoint.Endpoint) []*endpoint.Endpoint {
return result
}
// extractTarget populates the correct field given a record type.
// See dynect.DataBlock comments for details. Empty response means nothing
// was populated - basically an error
func extractTarget(recType string, data *dynect.DataBlock) string {
result := ""
if recType == endpoint.RecordTypeA {
result = data.Address
}
if recType == endpoint.RecordTypeCNAME {
result = data.CName
result = strings.TrimSuffix(result, ".")
}
if recType == endpoint.RecordTypeTXT {
result = data.TxtData
}
return result
}
func apiRetryLoop(f func() error) error {
var err error
for i := 0; i < dynMaxRetriesOnErrRateLimited; i++ {
@ -315,40 +248,48 @@ func apiRetryLoop(f func() error) error {
return err
}
// recordLinkToEndpoint makes an Endpoint given a resource link optinally making a remote call if a cached entry is expired
func (d *dynProviderState) recordLinkToEndpoint(client *dynect.Client, recordLink string) (*endpoint.Endpoint, error) {
result := d.Cache.Get(recordLink)
if result != nil {
log.Infof("Using cached endpoint for %s: %+v", recordLink, result)
return result, nil
func (d *dynProviderState) allRecordsToEndpoints(records *dynectsoap.GetAllRecordsResponseType) []*endpoint.Endpoint {
result := []*endpoint.Endpoint{}
//Convert each record to an endpoint
//Process A Records
for _, rec := range records.Data.A_records {
ep := &endpoint.Endpoint{
DNSName: rec.Fqdn,
RecordTTL: endpoint.TTL(rec.Ttl),
RecordType: rec.Record_type,
Targets: endpoint.Targets{rec.Rdata.Address},
}
log.Debugf("A record: %v", *ep)
result = append(result, ep)
}
rec := dynect.RecordResponse{}
err := apiRetryLoop(func() error {
return client.Do("GET", recordLink, nil, &rec)
})
if err != nil {
return nil, err
//Process CNAME Records
for _, rec := range records.Data.Cname_records {
ep := &endpoint.Endpoint{
DNSName: rec.Fqdn,
RecordTTL: endpoint.TTL(rec.Ttl),
RecordType: rec.Record_type,
Targets: endpoint.Targets{strings.TrimSuffix(rec.Rdata.Cname, ".")},
}
log.Debugf("CNAME record: %v", *ep)
result = append(result, ep)
}
// ignore all records but the types supported by external-
target := extractTarget(rec.Data.RecordType, &rec.Data.RData)
if target == "" {
return nil, nil
//Process TXT Records
for _, rec := range records.Data.Txt_records {
ep := &endpoint.Endpoint{
DNSName: rec.Fqdn,
RecordTTL: endpoint.TTL(rec.Ttl),
RecordType: rec.Record_type,
Targets: endpoint.Targets{rec.Rdata.Txtdata},
}
log.Debugf("TXT record: %v", *ep)
result = append(result, ep)
}
result = &endpoint.Endpoint{
DNSName: rec.Data.FQDN,
RecordTTL: endpoint.TTL(rec.Data.TTL),
RecordType: rec.Data.RecordType,
Targets: endpoint.Targets{target},
}
return result
log.Debugf("Fetched new endpoint for %s: %+v", recordLink, result)
d.Cache.Put(recordLink, result)
return result, nil
}
func errorOrValue(err error, value interface{}) interface{} {
@ -387,6 +328,72 @@ func (d *dynProviderState) fetchZoneSerial(client *dynect.Client, zone string) (
return resp.Data.Serial, nil
}
//Use SOAP to fetch all records with a single call
func (d *dynProviderState) fetchAllRecordsInZone(zone string) (*dynectsoap.GetAllRecordsResponseType, error) {
var err error
client := dynectsoap.NewClient("https://api2.dynect.net/SOAP/")
service := dynectsoap.NewDynect(client)
sessionRequest := dynectsoap.SessionLoginRequestType{
Customer_name: d.CustomerName,
User_name: d.Username,
Password: d.Password,
Fault_incompat: 0,
}
resp := dynectsoap.SessionLoginResponseType{}
err = apiRetryLoop(func() error {
return service.Do(&sessionRequest, &resp)
})
if err != nil {
return nil, err
}
token := resp.Data.Token
logoutRequest := dynectsoap.SessionLogoutRequestType{
Token: token,
Fault_incompat: 0,
}
logoutResponse := dynectsoap.SessionLogoutResponseType{}
defer service.Do(&logoutRequest, &logoutResponse)
req := dynectsoap.GetAllRecordsRequestType{
Token: token,
Zone: zone,
Fault_incompat: 0,
}
records := dynectsoap.GetAllRecordsResponseType{}
err = apiRetryLoop(func() error {
return service.Do(&req, &records)
})
if err != nil {
return nil, err
}
log.Debugf("Got all Records, status is %s", records.Status)
if strings.ToLower(records.Status) == "incomplete" {
jobRequest := dynectsoap.GetJobRequestType{
Token: token,
Job_id: records.Job_id,
Fault_incompat: 0,
}
jobResults := dynectsoap.GetJobResponseType{}
err = apiRetryLoop(func() error {
return service.GetJobRetry(&jobRequest, &jobResults)
})
if err != nil {
return nil, err
}
return jobResults.Data.(*dynectsoap.GetAllRecordsResponseType), nil
}
return &records, nil
}
// fetchAllRecordLinksInZone list all records in a zone with a single call. Records not matched by the
// DomainFilter are ignored. The response is a list of links that can be fed to dynect.Client.Do()
// directly
@ -611,22 +618,14 @@ func (d *dynProviderState) Records() ([]*endpoint.Endpoint, error) {
continue
}
recordLinks, err := d.fetchAllRecordLinksInZone(client, zone)
//Fetch All Records
records, err := d.fetchAllRecordsInZone(zone)
if err != nil {
return nil, err
}
relevantRecords = d.allRecordsToEndpoints(records)
log.Infof("Found %d relevant records found in zone %s: %+v", len(recordLinks), zone, recordLinks)
for _, link := range recordLinks {
ep, err := d.recordLinkToEndpoint(client, link)
if err != nil {
return nil, err
}
if ep != nil {
relevantRecords = append(relevantRecords, ep)
}
}
log.Debugf("Relevant records %+v", relevantRecords)
d.ZoneSnapshot.StoreRecordsForSerial(zone, serial, relevantRecords)
log.Infof("Stored %d records for %s@%d", len(relevantRecords), zone, serial)

View File

@ -20,7 +20,6 @@ import (
"errors"
"fmt"
"testing"
"time"
"github.com/nesv/go-dynect/dynect"
"github.com/stretchr/testify/assert"
@ -169,22 +168,6 @@ func TestDynMerge_NoUpdateIfTTLUnchanged(t *testing.T) {
assert.Equal(t, 0, len(merged))
}
func TestDyn_extractTarget(t *testing.T) {
tests := []struct {
recordType string
block *dynect.DataBlock
target string
}{
{"A", &dynect.DataBlock{Address: "address"}, "address"},
{"CNAME", &dynect.DataBlock{CName: "name."}, "name"}, // note trailing dot is trimmed for CNAMEs
{"TXT", &dynect.DataBlock{TxtData: "text."}, "text."},
}
for _, tc := range tests {
assert.Equal(t, tc.target, extractTarget(tc.recordType, tc.block))
}
}
func TestDyn_endpointToRecord(t *testing.T) {
tests := []struct {
ep *endpoint.Endpoint
@ -264,42 +247,6 @@ func TestDyn_fixMissingTTL(t *testing.T) {
assert.Equal(t, "1992", fixMissingTTL(endpoint.TTL(111), 1992))
}
func TestDyn_cachePut(t *testing.T) {
c := cache{
contents: make(map[string]*entry),
}
c.Put("link", &endpoint.Endpoint{
DNSName: "name",
Targets: endpoint.Targets{"target"},
RecordTTL: endpoint.TTL(10000),
RecordType: "A",
})
found := c.Get("link")
assert.NotNil(t, found)
}
func TestDyn_cachePutExpired(t *testing.T) {
c := cache{
contents: make(map[string]*entry),
}
c.Put("link", &endpoint.Endpoint{
DNSName: "name",
Targets: endpoint.Targets{"target"},
RecordTTL: endpoint.TTL(0),
RecordType: "A",
})
time.Sleep(2 * time.Second)
found := c.Get("link")
assert.Nil(t, found)
assert.Nil(t, c.Get("no-such-records"))
}
func TestDyn_Snapshot(t *testing.T) {
snap := ZoneSnapshot{
serials: map[string]int{},

View File

@ -18,7 +18,6 @@ package provider
import (
"fmt"
"sort"
"strings"
"cloud.google.com/go/compute/metadata"
@ -204,18 +203,7 @@ func (p *GoogleProvider) Records() (endpoints []*endpoint.Endpoint, _ error) {
if !supportedRecordType(r.Type) {
continue
}
ep := &endpoint.Endpoint{
DNSName: strings.TrimSuffix(r.Name, "."),
RecordType: r.Type,
Targets: make(endpoint.Targets, 0, len(r.Rrdatas)),
RecordTTL: endpoint.TTL(r.Ttl),
}
for _, rr := range r.Rrdatas {
// each page is processed sequentially, no need for a mutex here.
ep.Targets = append(ep.Targets, strings.TrimSuffix(rr, "."))
}
sort.Sort(ep.Targets)
endpoints = append(endpoints, ep)
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(r.Name, r.Type, endpoint.TTL(r.Ttl), r.Rrdatas...))
}
return nil

View File

@ -17,6 +17,7 @@ limitations under the License.
package provider
import (
"fmt"
"os"
"strconv"
"strings"
@ -95,10 +96,11 @@ func NewInfobloxProvider(infobloxConfig InfobloxConfig) (*InfobloxProvider, erro
func (p *InfobloxProvider) Records() (endpoints []*endpoint.Endpoint, err error) {
zones, err := p.zones()
if err != nil {
return nil, err
return nil, fmt.Errorf("could not fetch zones: %s", err)
}
for _, zone := range zones {
logrus.Debugf("fetch records from zone '%s'", zone.Fqdn)
var resA []ibclient.RecordA
objA := ibclient.NewRecordA(
ibclient.RecordA{
@ -107,7 +109,7 @@ func (p *InfobloxProvider) Records() (endpoints []*endpoint.Endpoint, err error)
)
err = p.client.GetObject(objA, "", &resA)
if err != nil {
return nil, err
return nil, fmt.Errorf("could not fetch A records from zone '%s': %s", zone.Fqdn, err)
}
for _, res := range resA {
endpoints = append(endpoints, endpoint.NewEndpoint(res.Name, endpoint.RecordTypeA, res.Ipv4Addr))
@ -122,7 +124,7 @@ func (p *InfobloxProvider) Records() (endpoints []*endpoint.Endpoint, err error)
)
err = p.client.GetObject(objH, "", &resH)
if err != nil {
return nil, err
return nil, fmt.Errorf("could not fetch host records from zone '%s': %s", zone.Fqdn, err)
}
for _, res := range resH {
for _, ip := range res.Ipv4Addrs {
@ -138,7 +140,7 @@ func (p *InfobloxProvider) Records() (endpoints []*endpoint.Endpoint, err error)
)
err = p.client.GetObject(objC, "", &resC)
if err != nil {
return nil, err
return nil, fmt.Errorf("could not fetch CNAME records from zone '%s': %s", zone.Fqdn, err)
}
for _, res := range resC {
endpoints = append(endpoints, endpoint.NewEndpoint(res.Name, endpoint.RecordTypeCNAME, res.Canonical))
@ -152,7 +154,7 @@ func (p *InfobloxProvider) Records() (endpoints []*endpoint.Endpoint, err error)
)
err = p.client.GetObject(objT, "", &resT)
if err != nil {
return nil, err
return nil, fmt.Errorf("could not fetch TXT records from zone '%s': %s", zone.Fqdn, err)
}
for _, res := range resT {
// The Infoblox API strips enclosing double quotes from TXT records lacking whitespace.
@ -163,6 +165,7 @@ func (p *InfobloxProvider) Records() (endpoints []*endpoint.Endpoint, err error)
endpoints = append(endpoints, endpoint.NewEndpoint(res.Name, endpoint.RecordTypeTXT, res.Text))
}
}
logrus.Debugf("fetched %d records from infoblox", len(endpoints))
return endpoints, nil
}

View File

@ -43,7 +43,7 @@ type rfc2136Provider struct {
// only consider hosted zones managing domains ending in this suffix
domainFilter DomainFilter
dryRun bool
actions rfc1236Actions
actions rfc2136Actions
}
var (
@ -56,13 +56,13 @@ var (
}
)
type rfc1236Actions interface {
type rfc2136Actions interface {
SendMessage(msg *dns.Msg) error
IncomeTransfer(m *dns.Msg, a string) (env chan *dns.Envelope, err error)
}
// NewRfc2136Provider is a factory function for OpenStack rfc2136 providers
func NewRfc2136Provider(host string, port int, zoneName string, insecure bool, keyName string, secret string, secretAlg string, axfr bool, domainFilter DomainFilter, dryRun bool, actions rfc1236Actions) (Provider, error) {
func NewRfc2136Provider(host string, port int, zoneName string, insecure bool, keyName string, secret string, secretAlg string, axfr bool, domainFilter DomainFilter, dryRun bool, actions rfc2136Actions) (Provider, error) {
secretAlgChecked, ok := tsigAlgs[secretAlg]
if !ok {
return nil, errors.Errorf("%s is not supported TSIG algorithm", secretAlg)
@ -240,25 +240,26 @@ func (r rfc2136Provider) UpdateRecord(ep *endpoint.Endpoint) error {
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)
newRR := fmt.Sprintf("%s %d %s %s", ep.DNSName, ep.RecordTTL, ep.RecordType, ep.Targets)
log.Debugf("Adding RR: %s", newRR)
rr, err := dns.NewRR(newRR)
if err != nil {
return fmt.Errorf("failed to build RR: %v", err)
}
rr, err := dns.NewRR(newRR)
if err != nil {
return fmt.Errorf("failed to build RR: %v", err)
}
rrs := make([]dns.RR, 1)
rrs[0] = rr
rrs := make([]dns.RR, 1)
rrs[0] = rr
m := new(dns.Msg)
m.SetUpdate(r.zoneName)
m.Insert(rrs)
m := new(dns.Msg)
m.SetUpdate(r.zoneName)
m.Insert(rrs)
err = r.actions.SendMessage(m)
if err != nil {
return fmt.Errorf("RFC2136 query failed: %v", err)
err = r.actions.SendMessage(m)
if err != nil {
return fmt.Errorf("RFC2136 query failed: %v", err)
}
}
return nil
@ -293,6 +294,7 @@ func (r rfc2136Provider) RemoveRecord(ep *endpoint.Endpoint) error {
func (r rfc2136Provider) SendMessage(msg *dns.Msg) error {
if r.dryRun {
log.Debugf("SendMessage.skipped")
return nil
}
log.Debugf("SendMessage")

View File

@ -93,7 +93,7 @@ func createRfc2136StubProvider(stub *rfc2136Stub) (Provider, error) {
return NewRfc2136Provider("", 0, "", false, "key", "secret", "hmac-sha512", true, DomainFilter{}, false, stub)
}
func TestRfc1236GetRecords(t *testing.T) {
func TestRfc2136GetRecords(t *testing.T) {
stub := newStub()
err := stub.setOutput([]string{
"v4.barfoo.com 3600 TXT test1",

View File

@ -0,0 +1,57 @@
/*
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 (
"strings"
)
// ZoneTagFilter holds a list of zone tags to filter by
type ZoneTagFilter struct {
zoneTags []string
}
// NewZoneTagFilter returns a new ZoneTagFilter given a list of zone tags
func NewZoneTagFilter(tags []string) ZoneTagFilter {
if len(tags) == 1 && len(tags[0]) == 0 {
tags = []string{}
}
return ZoneTagFilter{zoneTags: tags}
}
// Match checks whether a zone's set of tags matches the provided tag values
func (f ZoneTagFilter) Match(tagsMap map[string]string) bool {
for _, tagFilter := range f.zoneTags {
filterParts := strings.SplitN(tagFilter, "=", 2)
switch len(filterParts) {
case 1:
if _, hasTag := tagsMap[filterParts[0]]; !hasTag {
return false
}
case 2:
if value, hasTag := tagsMap[filterParts[0]]; !hasTag || value != filterParts[1] {
return false
}
}
}
return true
}
// IsEmpty returns true if there are no tags for the filter
func (f ZoneTagFilter) IsEmpty() bool {
return len(f.zoneTags) == 0
}

View File

@ -0,0 +1,62 @@
/*
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 (
"testing"
"github.com/stretchr/testify/assert"
)
func TestZoneTagFilterMatch(t *testing.T) {
for _, tc := range []struct {
name string
zoneTagFilter []string
zoneTags map[string]string
matches bool
}{
{
"single tag no match", []string{"tag1=value1"}, map[string]string{"tag0": "value0"}, false,
},
{
"single tag matches", []string{"tag1=value1"}, map[string]string{"tag1": "value1"}, true,
},
{
"multiple tags no value match", []string{"tag1=value1"}, map[string]string{"tag0": "value0", "tag1": "value2"}, false,
},
{
"multiple tags matches", []string{"tag1=value1"}, map[string]string{"tag0": "value0", "tag1": "value1"}, true,
},
{
"tag name no match", []string{"tag1"}, map[string]string{"tag0": "value0"}, false,
},
{
"tag name matches", []string{"tag1"}, map[string]string{"tag1": "value1"}, true,
},
{
"multiple filter no match", []string{"tag1=value1", "tag2=value2"}, map[string]string{"tag1": "value1"}, false,
},
{
"multiple filter matches", []string{"tag1=value1", "tag2=value2"}, map[string]string{"tag2": "value2", "tag1": "value1", "tag3": "value3"}, true,
},
} {
zoneTagFilter := NewZoneTagFilter(tc.zoneTagFilter)
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.matches, zoneTagFilter.Match(tc.zoneTags))
})
}
}

View File

@ -69,7 +69,7 @@ func NewCRDClientForAPIVersionKind(client kubernetes.Interface, kubeConfig, kube
}
apiResourceList, err := client.Discovery().ServerResourcesForGroupVersion(groupVersion.String())
if err != nil {
return nil, nil, fmt.Errorf("error listing resources in GroupVersion %q", groupVersion.String())
return nil, nil, fmt.Errorf("error listing resources in GroupVersion %q: %s", groupVersion.String(), err)
}
var crdAPIResource *metav1.APIResource
@ -118,6 +118,11 @@ func (cs *crdSource) Endpoints() ([]*endpoint.Endpoint, error) {
for _, dnsEndpoint := range result.Items {
endpoints = append(endpoints, dnsEndpoint.Spec.Endpoints...)
if dnsEndpoint.Status.ObservedGeneration == dnsEndpoint.Generation {
continue
}
dnsEndpoint.Status.ObservedGeneration = dnsEndpoint.Generation
// Update the ObservedGeneration
_, err = cs.UpdateStatus(&dnsEndpoint)

View File

@ -18,6 +18,7 @@ package source
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
@ -54,20 +55,21 @@ func objBody(codec runtime.Codec, obj runtime.Object) io.ReadCloser {
return ioutil.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(codec, obj))))
}
func startCRDServerToServeTargets(endpoints []*endpoint.Endpoint, apiVersion, kind, namespace, name string) rest.Interface {
func startCRDServerToServeTargets(endpoints []*endpoint.Endpoint, apiVersion, kind, namespace, name string, t *testing.T) rest.Interface {
groupVersion, _ := schema.ParseGroupVersion(apiVersion)
scheme := runtime.NewScheme()
addKnownTypes(scheme, groupVersion)
dnsEndpointList := endpoint.DNSEndpointList{}
dnsEndpoint := endpoint.DNSEndpoint{
dnsEndpoint := &endpoint.DNSEndpoint{
TypeMeta: metav1.TypeMeta{
APIVersion: apiVersion,
Kind: kind,
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Name: name,
Namespace: namespace,
Generation: 1,
},
Spec: endpoint.DNSEndpointSpec{
Endpoints: endpoints,
@ -88,10 +90,18 @@ func startCRDServerToServeTargets(endpoints []*endpoint.Endpoint, apiVersion, ki
case p == "/apis/"+apiVersion+"/"+strings.ToLower(kind)+"s" && m == http.MethodGet:
fallthrough
case p == "/apis/"+apiVersion+"/namespaces/"+namespace+"/"+strings.ToLower(kind)+"s" && m == http.MethodGet:
dnsEndpointList.Items = append(dnsEndpointList.Items, dnsEndpoint)
dnsEndpointList.Items = dnsEndpointList.Items[:0]
dnsEndpointList.Items = append(dnsEndpointList.Items, *dnsEndpoint)
return &http.Response{StatusCode: http.StatusOK, Header: defaultHeader(), Body: objBody(codec, &dnsEndpointList)}, nil
case strings.HasPrefix(p, "/apis/"+apiVersion+"/namespaces/") && strings.HasSuffix(p, strings.ToLower(kind)+"s") && m == http.MethodGet:
return &http.Response{StatusCode: http.StatusOK, Header: defaultHeader(), Body: objBody(codec, &dnsEndpointList)}, nil
case p == "/apis/"+apiVersion+"/namespaces/"+namespace+"/"+strings.ToLower(kind)+"s/"+name+"/status" && m == http.MethodPut:
decoder := json.NewDecoder(req.Body)
var body endpoint.DNSEndpoint
decoder.Decode(&body)
dnsEndpoint.Status.ObservedGeneration = body.Status.ObservedGeneration
return &http.Response{StatusCode: http.StatusOK, Header: defaultHeader(), Body: objBody(codec, dnsEndpoint)}, nil
default:
return nil, fmt.Errorf("unexpected request: %#v\n%#v", req.URL, req)
}
@ -200,6 +210,8 @@ func testCRDSourceEndpoints(t *testing.T) {
apiVersion: "test.k8s.io/v1alpha1",
registeredKind: "DNSEndpoint",
kind: "DNSEndpoint",
namespace: "foo",
registeredNamespace: "foo",
endpoints: []*endpoint.Endpoint{
{DNSName: "abc.example.org",
Targets: endpoint.Targets{"1.2.3.4"},
@ -216,6 +228,8 @@ func testCRDSourceEndpoints(t *testing.T) {
apiVersion: "test.k8s.io/v1alpha1",
registeredKind: "DNSEndpoint",
kind: "DNSEndpoint",
namespace: "foo",
registeredNamespace: "foo",
endpoints: []*endpoint.Endpoint{
{DNSName: "abc.example.org",
Targets: endpoint.Targets{"1.2.3.4"},
@ -233,7 +247,7 @@ func testCRDSourceEndpoints(t *testing.T) {
},
} {
t.Run(ti.title, func(t *testing.T) {
restClient := startCRDServerToServeTargets(ti.endpoints, ti.registeredAPIVersion, ti.registeredKind, ti.registeredNamespace, "")
restClient := startCRDServerToServeTargets(ti.endpoints, ti.registeredAPIVersion, ti.registeredKind, ti.registeredNamespace, "test", t)
groupVersion, err := schema.ParseGroupVersion(ti.apiVersion)
require.NoError(t, err)
@ -253,8 +267,28 @@ func testCRDSourceEndpoints(t *testing.T) {
return
}
if err == nil {
validateCRDResource(t, cs, ti.expectError)
}
// Validate received endpoints against expected endpoints.
validateEndpoints(t, receivedEndpoints, ti.endpoints)
})
}
}
func validateCRDResource(t *testing.T, src Source, expectError bool) {
cs := src.(*crdSource)
result, err := cs.List(&metav1.ListOptions{})
if expectError {
require.Errorf(t, err, "Received err %v", err)
} else {
require.NoErrorf(t, err, "Received err %v", err)
}
for _, dnsEndpoint := range result.Items {
if dnsEndpoint.Status.ObservedGeneration != dnsEndpoint.Generation {
require.Errorf(t, err, "Unexpected CRD resource result: ObservedGenerations <%v> is not equal to Generation<%v>", dnsEndpoint.Status.ObservedGeneration, dnsEndpoint.Generation)
}
}
}

View File

@ -171,6 +171,7 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri
return endpoints
}
targetsByHeadlessDomain := make(map[string][]string)
for _, v := range pods.Items {
headlessDomain := hostname
if v.Spec.Hostname != "" {
@ -181,11 +182,7 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri
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 {
if ttl.IsConfigured() {
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(headlessDomain, endpoint.RecordTypeA, ttl, v.Status.HostIP))
} else {
endpoints = append(endpoints, endpoint.NewEndpoint(headlessDomain, endpoint.RecordTypeA, v.Status.HostIP))
}
targetsByHeadlessDomain[headlessDomain] = append(targetsByHeadlessDomain[headlessDomain], v.Status.HostIP)
} else {
log.Debugf("Pod %s is not in running phase", v.Spec.Hostname)
}
@ -193,11 +190,7 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri
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 {
if ttl.IsConfigured() {
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(headlessDomain, endpoint.RecordTypeA, ttl, v.Status.PodIP))
} else {
endpoints = append(endpoints, endpoint.NewEndpoint(headlessDomain, endpoint.RecordTypeA, v.Status.PodIP))
}
targetsByHeadlessDomain[headlessDomain] = append(targetsByHeadlessDomain[headlessDomain], v.Status.PodIP)
} else {
log.Debugf("Pod %s is not in running phase", v.Spec.Hostname)
}
@ -205,6 +198,20 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri
}
headlessDomains := []string{}
for headlessDomain := range targetsByHeadlessDomain {
headlessDomains = append(headlessDomains, headlessDomain)
}
sort.Strings(headlessDomains)
for _, headlessDomain := range headlessDomains {
targets := targetsByHeadlessDomain[headlessDomain]
if ttl.IsConfigured() {
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(headlessDomain, endpoint.RecordTypeA, ttl, targets...))
} else {
endpoints = append(endpoints, endpoint.NewEndpoint(headlessDomain, endpoint.RecordTypeA, targets...))
}
}
return endpoints
}

View File

@ -1321,7 +1321,7 @@ func TestHeadlessServices(t *testing.T) {
labels map[string]string
annotations map[string]string
clusterIP string
podIP string
podIPs []string
selector map[string]string
lbs []string
podnames []string
@ -1343,7 +1343,7 @@ func TestHeadlessServices(t *testing.T) {
hostnameAnnotationKey: "service.example.org",
},
v1.ClusterIPNone,
"1.1.1.1",
[]string{"1.1.1.1", "1.1.1.2"},
map[string]string{
"component": "foo",
},
@ -1353,7 +1353,7 @@ func TestHeadlessServices(t *testing.T) {
[]v1.PodPhase{v1.PodRunning, v1.PodRunning},
[]*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.1"}},
{DNSName: "foo-1.service.example.org", Targets: endpoint.Targets{"1.1.1.2"}},
},
false,
},
@ -1371,7 +1371,7 @@ func TestHeadlessServices(t *testing.T) {
ttlAnnotationKey: "1",
},
v1.ClusterIPNone,
"1.1.1.1",
[]string{"1.1.1.1", "1.1.1.2"},
map[string]string{
"component": "foo",
},
@ -1381,7 +1381,7 @@ func TestHeadlessServices(t *testing.T) {
[]v1.PodPhase{v1.PodRunning, v1.PodRunning},
[]*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.1"}, RecordTTL: endpoint.TTL(1)},
{DNSName: "foo-1.service.example.org", Targets: endpoint.Targets{"1.1.1.2"}, RecordTTL: endpoint.TTL(1)},
},
false,
},
@ -1398,7 +1398,7 @@ func TestHeadlessServices(t *testing.T) {
hostnameAnnotationKey: "service.example.org",
},
v1.ClusterIPNone,
"1.1.1.1",
[]string{"1.1.1.1", "1.1.1.2"},
map[string]string{
"component": "foo",
},
@ -1424,7 +1424,7 @@ func TestHeadlessServices(t *testing.T) {
hostnameAnnotationKey: "service.example.org",
},
v1.ClusterIPNone,
"1.1.1.1",
[]string{"1.1.1.1", "1.1.1.2"},
map[string]string{
"component": "foo",
},
@ -1433,8 +1433,7 @@ func TestHeadlessServices(t *testing.T) {
[]string{"", ""},
[]v1.PodPhase{v1.PodRunning, v1.PodRunning},
[]*endpoint.Endpoint{
{DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1"}},
{DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1"}},
{DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}},
},
false,
},
@ -1473,7 +1472,7 @@ func TestHeadlessServices(t *testing.T) {
Annotations: tc.annotations,
},
Status: v1.PodStatus{
PodIP: tc.podIP,
PodIP: tc.podIPs[i],
Phase: tc.phases[i],
},
}
@ -1522,7 +1521,7 @@ func TestHeadlessServicesHostIP(t *testing.T) {
labels map[string]string
annotations map[string]string
clusterIP string
hostIP string
hostIPs []string
selector map[string]string
lbs []string
podnames []string
@ -1544,7 +1543,7 @@ func TestHeadlessServicesHostIP(t *testing.T) {
hostnameAnnotationKey: "service.example.org",
},
v1.ClusterIPNone,
"1.1.1.1",
[]string{"1.1.1.1", "1.1.1.2"},
map[string]string{
"component": "foo",
},
@ -1554,7 +1553,7 @@ func TestHeadlessServicesHostIP(t *testing.T) {
[]v1.PodPhase{v1.PodRunning, v1.PodRunning},
[]*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.1"}},
{DNSName: "foo-1.service.example.org", Targets: endpoint.Targets{"1.1.1.2"}},
},
false,
},
@ -1572,7 +1571,7 @@ func TestHeadlessServicesHostIP(t *testing.T) {
ttlAnnotationKey: "1",
},
v1.ClusterIPNone,
"1.1.1.1",
[]string{"1.1.1.1", "1.1.1.2"},
map[string]string{
"component": "foo",
},
@ -1582,7 +1581,7 @@ func TestHeadlessServicesHostIP(t *testing.T) {
[]v1.PodPhase{v1.PodRunning, v1.PodRunning},
[]*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.1"}, RecordTTL: endpoint.TTL(1)},
{DNSName: "foo-1.service.example.org", Targets: endpoint.Targets{"1.1.1.2"}, RecordTTL: endpoint.TTL(1)},
},
false,
},
@ -1599,7 +1598,7 @@ func TestHeadlessServicesHostIP(t *testing.T) {
hostnameAnnotationKey: "service.example.org",
},
v1.ClusterIPNone,
"1.1.1.1",
[]string{"1.1.1.1", "1.1.1.2"},
map[string]string{
"component": "foo",
},
@ -1625,7 +1624,7 @@ func TestHeadlessServicesHostIP(t *testing.T) {
hostnameAnnotationKey: "service.example.org",
},
v1.ClusterIPNone,
"1.1.1.1",
[]string{"1.1.1.1", "1.1.1.2"},
map[string]string{
"component": "foo",
},
@ -1634,8 +1633,7 @@ func TestHeadlessServicesHostIP(t *testing.T) {
[]string{"", ""},
[]v1.PodPhase{v1.PodRunning, v1.PodRunning},
[]*endpoint.Endpoint{
{DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1"}},
{DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1"}},
{DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}},
},
false,
},
@ -1674,7 +1672,7 @@ func TestHeadlessServicesHostIP(t *testing.T) {
Annotations: tc.annotations,
},
Status: v1.PodStatus{
HostIP: tc.hostIP,
HostIP: tc.hostIPs[i],
Phase: tc.phases[i],
},
}