mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-06 01:26:59 +02:00
Merge branch 'master' into cloudflare-proxied-annotation
This commit is contained in:
commit
6d39526069
@ -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)
|
||||
|
25
CHANGELOG.md
25
CHANGELOG.md
@ -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
84
Gopkg.lock
generated
@ -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
|
||||
|
@ -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"
|
@ -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
16
SECURITY_CONTACTS
Normal 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
|
@ -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},
|
||||
|
@ -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"
|
||||
|
@ -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).
|
||||
|
||||
|
@ -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
|
||||
|
29
docs/faq.md
29
docs/faq.md
@ -28,9 +28,9 @@ ExternalDNS can solve this for you as well.
|
||||
|
||||
### Which DNS providers are supported?
|
||||
|
||||
Currently, the following providers are supported:
|
||||
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 `,`
|
||||
|
@ -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.
|
||||
|
@ -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
235
docs/tutorials/coredns.md
Normal 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#
|
||||
```
|
@ -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.
|
||||
|
||||
@ -31,4 +64,4 @@ key "externaldns-key" {
|
||||
- `rfc2136-tsig-keyname` - this is string parameter with secret key name it is should `MATCH!` with server key name. In example it is `externaldns-key`.
|
||||
|
||||
|
||||
|
||||
|
||||
|
3
main.go
3
main.go
@ -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,
|
||||
},
|
||||
)
|
||||
|
@ -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)
|
||||
|
@ -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",
|
||||
|
21
plan/plan.go
21
plan/plan.go
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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) {
|
||||
|
213
provider/dyn.go
213
provider/dyn.go
@ -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)
|
||||
|
@ -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{},
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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")
|
||||
|
||||
|
@ -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",
|
||||
|
57
provider/zone_tag_filter.go
Normal file
57
provider/zone_tag_filter.go
Normal 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
|
||||
}
|
62
provider/zone_tag_filter_test.go
Normal file
62
provider/zone_tag_filter_test.go
Normal 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))
|
||||
})
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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],
|
||||
},
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user