Unverified Commit b73f6779 authored by xunpan's avatar xunpan Committed by GitHub
Browse files

Merge branch 'master' into config-prefix

parents 1260d7b4 411ab7f0
......@@ -91,6 +91,7 @@ The following table clarifies the current status of the providers according to t
| NS1 | Alpha |
| TransIP | Alpha |
| VinylDNS | Alpha |
| RancherDNS | Alpha |
## Running ExternalDNS:
......@@ -122,6 +123,7 @@ The following tutorials are provided:
* [RFC2136](docs/tutorials/rfc2136.md)
* [NS1](docs/tutorials/ns1.md)
* [TransIP](docs/tutorials/transip.md)
* [RancherDNS](docs/tutorials/rdns.md)
### Running Locally
......
# Setting up ExternalDNS for RancherDNS(RDNS) with kubernetes
This tutorial describes how to setup ExternalDNS for usage within a kubernetes cluster that makes use of [RDNS](https://github.com/rancher/rdns) and [nginx ingress controller](https://github.com/kubernetes/ingress-nginx).
You need to:
* install RDNS with [etcd](https://github.com/etcd-io/etcd) enabled
* install external-dns with rdns as a provider
## Installing RDNS with etcdv3 backend
### Clone RDNS
```
git clone https://github.com/rancher/rdns-server.git
```
### Installing ETCD
```
cd rdns-server
docker-compose -f deploy/etcdv3/etcd-compose.yaml up -d
```
> ETCD was successfully deployed on `http://172.31.35.77:2379`
### Installing RDNS
```
export ETCD_ENDPOINTS="http://172.31.35.77:2379"
export DOMAIN="lb.rancher.cloud"
./scripts/start etcdv3
```
> RDNS was successfully deployed on `172.31.35.77`
## Installing ExternalDNS
### Install external ExternalDNS
ETCD_URLS is configured to etcd client service address.
RDNS_ROOT_DOMAIN is configured to the same with RDNS DOMAIN environment. e.g. lb.rancher.cloud.
#### Manifest (for clusters without RBAC enabled)
```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:
serviceAccountName: external-dns
containers:
- name: external-dns
image: registry.opensource.zalan.do/teapot/external-dns:latest
args:
- --source=ingress
- --provider=rdns
- --log-level=debug # debug only
env:
- name: ETCD_URLS
value: http://172.31.35.77:2379
- name: RDNS_ROOT_DOMAIN
value: lb.rancher.cloud
```
#### Manifest (for clusters with RBAC enabled)
```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=rdns
- --log-level=debug # debug only
env:
- name: ETCD_URLS
value: http://172.31.35.77:2379
- name: RDNS_ROOT_DOMAIN
value: lb.rancher.cloud
```
## Testing ingress example
```
$ cat ingress.yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: nginx
annotations:
kubernetes.io/ingress.class: "nginx"
spec:
rules:
- host: nginx.lb.rancher.cloud
http:
paths:
- backend:
serviceName: nginx
servicePort: 80
$ kubectl apply -f ingress.yaml
ingress.extensions "nginx" created
```
Wait a moment until DNS has the ingress IP. The RDNS IP in this example is "172.31.35.77".
```
$ kubectl get ingress
NAME HOSTS ADDRESS PORTS AGE
nginx nginx.lb.rancher.cloud 172.31.42.211 80 2m
$ kubectl run -it --rm --restart=Never --image=infoblox/dnstools:latest dnstools
If you don't see a command prompt, try pressing enter.
dnstools# dig @172.31.35.77 nginx.lb.rancher.cloud +short
172.31.42.211
dnstools#
```
\ No newline at end of file
......@@ -177,6 +177,13 @@ func main() {
)
case "coredns", "skydns":
p, err = provider.NewCoreDNSProvider(domainFilter, cfg.CoreDNSPrefix, cfg.DryRun)
case "rdns":
p, err = provider.NewRDNSProvider(
provider.RDNSConfig{
DomainFilter: domainFilter,
DryRun: cfg.DryRun,
},
)
case "exoscale":
p, err = provider.NewExoscaleProvider(cfg.ExoscaleEndpoint, cfg.ExoscaleAPIKey, cfg.ExoscaleAPISecret, cfg.DryRun, provider.ExoscaleWithDomain(domainFilter), provider.ExoscaleWithLogging()), nil
case "inmemory":
......
......@@ -278,7 +278,7 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("service-type-filter", "The service types to take care about (default: all, expected: ClusterIP, NodePort, LoadBalancer or ExternalName)").StringsVar(&cfg.ServiceTypeFilter)
// Flags related to providers
app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, aws-sd, google, azure, cloudflare, rcodezero, digitalocean, dnsimple, infoblox, dyn, designate, coredns, skydns, inmemory, pdns, oci, exoscale, linode, rfc2136, ns1, transip, vinyldns)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "aws-sd", "google", "azure", "alibabacloud", "cloudflare", "rcodezero", "digitalocean", "dnsimple", "infoblox", "dyn", "designate", "coredns", "skydns", "inmemory", "pdns", "oci", "exoscale", "linode", "rfc2136", "ns1", "transip", "vinyldns")
app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, aws-sd, google, azure, cloudflare, rcodezero, digitalocean, dnsimple, infoblox, dyn, designate, coredns, skydns, inmemory, pdns, oci, exoscale, linode, rfc2136, ns1, transip, vinyldns, rdns)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "aws-sd", "google", "azure", "alibabacloud", "cloudflare", "rcodezero", "digitalocean", "dnsimple", "infoblox", "dyn", "designate", "coredns", "skydns", "inmemory", "pdns", "oci", "exoscale", "linode", "rfc2136", "ns1", "transip", "vinyldns", "rdns")
app.Flag("domain-filter", "Limit possible target zones by a domain suffix; specify multiple times for multiple domains (optional)").Default("").StringsVar(&cfg.DomainFilter)
app.Flag("exclude-domains", "Exclude subdomains (optional)").Default("").StringsVar(&cfg.ExcludeDomains)
app.Flag("zone-id-filter", "Filter target zones by hosted zone id; specify multiple times for multiple zones (optional)").Default("").StringsVar(&cfg.ZoneIDFilter)
......
......@@ -345,6 +345,7 @@ func TestParseFlags(t *testing.T) {
"EXTERNAL_DNS_PDNS_SERVER": "http://ns.example.com:8081",
"EXTERNAL_DNS_PDNS_API_KEY": "some-secret-key",
"EXTERNAL_DNS_PDNS_TLS_ENABLED": "1",
"EXTERNAL_DNS_RDNS_ROOT_DOMAIN": "lb.rancher.cloud",
"EXTERNAL_DNS_TLS_CA": "/path/to/ca.crt",
"EXTERNAL_DNS_TLS_CLIENT_CERT": "/path/to/cert.pem",
"EXTERNAL_DNS_TLS_CLIENT_CERT_KEY": "/path/to/key.pem",
......
/*
Copyright 2019 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package provider
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"io/ioutil"
"math/rand"
"os"
"regexp"
"strings"
"time"
"github.com/coreos/etcd/clientv3"
"github.com/pkg/errors"
"istio.io/istio/pkg/log"
"github.com/kubernetes-incubator/external-dns/endpoint"
"github.com/kubernetes-incubator/external-dns/plan"
)
const (
rdnsMaxHosts = 10
rdnsOriginalLabel = "originalText"
rdnsPrefix = "/rdnsv3"
rdnsTimeout = 5 * time.Second
)
func init() {
rand.Seed(time.Now().UnixNano())
}
// RDNSClient is an interface to work with Rancher DNS(RDNS) records in etcdv3 backend.
type RDNSClient interface {
Get(key string) ([]RDNSRecord, error)
List(rootDomain string) ([]RDNSRecord, error)
Set(value RDNSRecord) error
Delete(key string) error
}
// RDNSConfig contains configuration to create a new Rancher DNS(RDNS) provider.
type RDNSConfig struct {
DryRun bool
DomainFilter DomainFilter
RootDomain string
}
// RDNSProvider is an implementation of Provider for Rancher DNS(RDNS).
type RDNSProvider struct {
client RDNSClient
dryRun bool
domainFilter DomainFilter
rootDomain string
}
// RDNSRecord represents Rancher DNS(RDNS) etcdv3 record.
type RDNSRecord struct {
AggregationHosts []string `json:"aggregation_hosts,omitempty"`
Host string `json:"host,omitempty"`
Text string `json:"text,omitempty"`
TTL uint32 `json:"ttl,omitempty"`
Key string `json:"-"`
}
// RDNSRecordType represents Rancher DNS(RDNS) etcdv3 record type.
type RDNSRecordType struct {
Type string `json:"type,omitempty"`
Domain string `json:"domain,omitempty"`
}
type etcdv3Client struct {
client *clientv3.Client
ctx context.Context
}
var _ RDNSClient = etcdv3Client{}
// NewRDNSProvider initializes a new Rancher DNS(RDNS) based Provider.
func NewRDNSProvider(config RDNSConfig) (*RDNSProvider, error) {
client, err := newEtcdv3Client()
if err != nil {
return nil, err
}
domain := os.Getenv("RDNS_ROOT_DOMAIN")
if domain == "" {
return nil, errors.New("needed root domain environment")
}
return &RDNSProvider{
client: client,
dryRun: config.DryRun,
domainFilter: config.DomainFilter,
rootDomain: domain,
}, nil
}
// Records returns all DNS records found in Rancher DNS(RDNS) etcdv3 backend. Depending on the record fields
// it may be mapped to one or two records of type A, TXT, A+TXT.
func (p RDNSProvider) Records() ([]*endpoint.Endpoint, error) {
var result []*endpoint.Endpoint
rs, err := p.client.List(p.rootDomain)
if err != nil {
return nil, err
}
for _, r := range rs {
domains := strings.Split(strings.TrimPrefix(r.Key, rdnsPrefix+"/"), "/")
keyToDnsNameSplits(domains)
dnsName := strings.Join(domains, ".")
if !p.domainFilter.Match(dnsName) {
continue
}
// only return rdnsMaxHosts at most
if len(r.AggregationHosts) > 0 {
if len(r.AggregationHosts) > rdnsMaxHosts {
r.AggregationHosts = r.AggregationHosts[:rdnsMaxHosts]
}
ep := endpoint.NewEndpointWithTTL(
dnsName,
endpoint.RecordTypeA,
endpoint.TTL(r.TTL),
r.AggregationHosts...,
)
ep.Labels[rdnsOriginalLabel] = r.Text
result = append(result, ep)
}
if r.Text != "" {
ep := endpoint.NewEndpoint(
dnsName,
endpoint.RecordTypeTXT,
r.Text,
)
result = append(result, ep)
}
}
return result, nil
}
// ApplyChanges stores changes back to etcdv3 converting them to Rancher DNS(RDNS) format and aggregating A and TXT records.
func (p RDNSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
grouped := map[string][]*endpoint.Endpoint{}
for _, ep := range changes.Create {
grouped[ep.DNSName] = append(grouped[ep.DNSName], ep)
}
for _, ep := range changes.UpdateNew {
if ep.RecordType == endpoint.RecordTypeA {
// append useless domain records to the changes.Delete
if err := p.filterAndRemoveUseless(ep, changes); err != nil {
return err
}
}
grouped[ep.DNSName] = append(grouped[ep.DNSName], ep)
}
for dnsName, group := range grouped {
if !p.domainFilter.Match(dnsName) {
log.Debugf("Skipping record %s because it was filtered out by the specified --domain-filter", dnsName)
continue
}
var rs []RDNSRecord
for _, ep := range group {
if ep.RecordType == endpoint.RecordTypeTXT {
continue
}
for _, target := range ep.Targets {
rs = append(rs, RDNSRecord{
Host: target,
Text: ep.Labels[rdnsOriginalLabel],
Key: keyFor(ep.DNSName) + "/" + formatKey(target),
TTL: uint32(ep.RecordTTL),
})
}
}
// Add the TXT attribute to the existing A record
for _, ep := range group {
if ep.RecordType != endpoint.RecordTypeTXT {
continue
}
for i, r := range rs {
if strings.Contains(r.Key, keyFor(ep.DNSName)) {
r.Text = ep.Targets[0]
rs[i] = r
}
}
}
for _, r := range rs {
log.Infof("Add/set key %s to Host=%s, Text=%s, TTL=%d", r.Key, r.Host, r.Text, r.TTL)
if !p.dryRun {
err := p.client.Set(r)
if err != nil {
return err
}
}
}
}
for _, ep := range changes.Delete {
key := keyFor(ep.DNSName)
log.Infof("Delete key %s", key)
if !p.dryRun {
err := p.client.Delete(key)
if err != nil {
return err
}
}
}
return nil
}
// filterAndRemoveUseless filter and remove useless records.
func (p *RDNSProvider) filterAndRemoveUseless(ep *endpoint.Endpoint, changes *plan.Changes) error {
rs, err := p.client.Get(keyFor(ep.DNSName))
if err != nil {
return err
}
for _, r := range rs {
exist := false
for _, target := range ep.Targets {
if strings.Contains(r.Key, formatKey(target)) {
exist = true
continue
}
}
if !exist {
ds := strings.Split(strings.TrimPrefix(r.Key, rdnsPrefix+"/"), "/")
keyToDnsNameSplits(ds)
changes.Delete = append(changes.Delete, &endpoint.Endpoint{
DNSName: strings.Join(ds, "."),
})
}
}
return nil
}
// newEtcdv3Client is an etcdv3 client constructor.
func newEtcdv3Client() (RDNSClient, error) {
cfg := &clientv3.Config{}
endpoints := os.Getenv("ETCD_URLS")
ca := os.Getenv("ETCD_CA_FILE")
cert := os.Getenv("ETCD_CERT_FILE")
key := os.Getenv("ETCD_KEY_FILE")
name := os.Getenv("ETCD_TLS_SERVER_NAME")
insecure := os.Getenv("ETCD_TLS_INSECURE")
if endpoints == "" {
endpoints = "http://localhost:2379"
}
urls := strings.Split(endpoints, ",")
scheme := strings.ToLower(urls[0])[0:strings.Index(strings.ToLower(urls[0]), "://")]
switch scheme {
case "http":
cfg.Endpoints = urls
case "https":
var certificates []tls.Certificate
insecure = strings.ToLower(insecure)
isInsecure := insecure == "true" || insecure == "yes" || insecure == "1"
if ca != "" && key == "" || cert == "" && key != "" {
return nil, errors.New("either both cert and key or none must be provided")
}
if cert != "" {
cert, err := tls.LoadX509KeyPair(cert, key)
if err != nil {
return nil, fmt.Errorf("could not load TLS cert: %s", err)
}
certificates = append(certificates, cert)
}
config := &tls.Config{
Certificates: certificates,
InsecureSkipVerify: isInsecure,
ServerName: name,
}
if ca != "" {
roots := x509.NewCertPool()
pem, err := ioutil.ReadFile(ca)
if err != nil {
return nil, fmt.Errorf("error reading %s: %s", ca, err)
}
ok := roots.AppendCertsFromPEM(pem)
if !ok {
return nil, fmt.Errorf("could not read root certs: %s", err)
}
config.RootCAs = roots
}
cfg.Endpoints = urls
cfg.TLS = config
default:
return nil, errors.New("etcdv3 URLs must start with either http:// or https://")
}
c, err := clientv3.New(*cfg)
if err != nil {
return nil, err
}
return etcdv3Client{c, context.Background()}, nil
}
// Get return A records stored in etcdv3 stored anywhere under the given key (recursively).
func (c etcdv3Client) Get(key string) ([]RDNSRecord, error) {
ctx, cancel := context.WithTimeout(c.ctx, rdnsTimeout)
defer cancel()
result, err := c.client.Get(ctx, key, clientv3.WithPrefix())
if err != nil {
return nil, err
}
rs := make([]RDNSRecord, 0)
for _, v := range result.Kvs {
r := new(RDNSRecord)
if err := json.Unmarshal(v.Value, r); err != nil {
return nil, fmt.Errorf("%s: %s", v.Key, err.Error())
}
r.Key = string(v.Key)
rs = append(rs, *r)
}
return rs, nil
}
// List return all records stored in etcdv3 stored anywhere under the given rootDomain (recursively).
func (c etcdv3Client) List(rootDomain string) ([]RDNSRecord, error) {
ctx, cancel := context.WithTimeout(c.ctx, rdnsTimeout)
defer cancel()
path := keyFor(rootDomain)
result, err := c.client.Get(ctx, path, clientv3.WithPrefix())
if err != nil {
return nil, err
}
return c.aggregationRecords(result)
}
// Set persists records data into etcdv3.
func (c etcdv3Client) Set(r RDNSRecord) error {
ctx, cancel := context.WithTimeout(c.ctx, etcdTimeout)
defer cancel()
v, err := json.Marshal(&r)
if err != nil {
return err
}
if r.Text == "" && r.Host == "" {
return nil
}
_, err = c.client.Put(ctx, r.Key, string(v))
if err != nil {
return err
}
return nil
}
// Delete deletes record from etcdv3.
func (c etcdv3Client) Delete(key string) error {
ctx, cancel := context.WithTimeout(c.ctx, etcdTimeout)
defer cancel()
_, err := c.client.Delete(ctx, key, clientv3.WithPrefix())
return err
}
// aggregationRecords will aggregation multi A records under the given path.
// e.g. A: 1_1_1_1.xxx.lb.rancher.cloud & 2_2_2_2.sample.lb.rancher.cloud => sample.lb.rancher.cloud {"aggregation_hosts": ["1.1.1.1", "2.2.2.2"]}
// e.g. TXT: sample.lb.rancher.cloud => sample.lb.rancher.cloud => {"text": "xxx"}
func (c etcdv3Client) aggregationRecords(result *clientv3.GetResponse) ([]RDNSRecord, error) {
var rs []RDNSRecord
bx := make(map[RDNSRecordType]RDNSRecord)
for _, n := range result.Kvs {
r := new(RDNSRecord)
if err := json.Unmarshal(n.Value, r); err != nil {
return nil, fmt.Errorf("%s: %s", n.Key, err.Error())
}
r.Key = string(n.Key)
if r.Host == "" && r.Text == "" {
continue
}
if r.Host != "" {
c := RDNSRecord{
AggregationHosts: r.AggregationHosts,
Host: r.Host,
Text: r.Text,
TTL: r.TTL,
Key: r.Key,
}
n, isContinue := appendRecords(c, endpoint.RecordTypeA, bx, rs)
if isContinue {
continue
}
rs = n
}
if r.Text != "" && r.Host == "" {
c := RDNSRecord{
AggregationHosts: []string{},
Host: r.Host,
Text: r.Text,
TTL: r.TTL,
Key: r.Key,
}
n, isContinue := appendRecords(c, endpoint.RecordTypeTXT, bx, rs)
if isContinue {
continue
}
rs = n
}
}
return rs, nil
}
// appendRecords append record to an array
func appendRecords(r RDNSRecord, dnsType string, bx map[RDNSRecordType]RDNSRecord, rs []RDNSRecord) ([]RDNSRecord, bool) {
dnsName := keyToParentDnsName(r.Key)
bt := RDNSRecordType{Domain: dnsName, Type: dnsType}
if v, ok := bx[bt]; ok {
// skip the TXT records if already added to record list.
// append A record if dnsName already added to record list but not found the value.
// the same record might be found in multiple etcdv3 nodes.
if bt.Type == endpoint.RecordTypeA {
exist := false
for _, h := range v.AggregationHosts {
if h == r.Host {
exist = true
break
}
}
if !exist {
for i, t := range rs {
if !strings.HasPrefix(r.Key, t.Key) {
continue
}
t.Host = ""
t.AggregationHosts = append(t.AggregationHosts, r.Host)
bx[bt] = t
rs[i] = t
}
}
}
return rs, true
}
if bt.Type == endpoint.RecordTypeA {
r.AggregationHosts = append(r.AggregationHosts, r.Host)
}
r.Key = rdnsPrefix + dnsNameToKey(dnsName)
r.Host = ""
bx[bt] = r
rs = append(rs, r)
return rs, false
}
// keyFor used to get a path as etcdv3 preferred.
// e.g. sample.lb.rancher.cloud => /rdnsv3/cloud/rancher/lb/sample
func keyFor(fqdn string) string {
return rdnsPrefix + dnsNameToKey(fqdn)
}
// keyToParentDnsName used to get dnsName.
// e.g. /rdnsv3/cloud/rancher/lb/sample/xxx => xxx.sample.lb.rancher.cloud
// e.g. /rdnsv3/cloud/rancher/lb/sample/xxx/1_1_1_1 => xxx.sample.lb.rancher.cloud
func keyToParentDnsName(key string) string {
ds := strings.Split(strings.TrimPrefix(key, rdnsPrefix+"/"), "/")
keyToDnsNameSplits(ds)
dns := strings.Join(ds, ".")
prefix := strings.Split(dns, ".")[0]
p := `^\d{1,3}_\d{1,3}_\d{1,3}_\d{1,3}$`
m, _ := regexp.MatchString(p, prefix)
if prefix != "" && strings.Contains(prefix, "_") && m {
// 1_1_1_1.xxx.sample.lb.rancher.cloud => xxx.sample.lb.rancher.cloud
return strings.Join(strings.Split(dns, ".")[1:], ".")
}
return dns
}
// dnsNameToKey used to convert domain to a path as etcdv3 preferred.
// e.g. sample.lb.rancher.cloud => /cloud/rancher/lb/sample
func dnsNameToKey(domain string) string {
ss := strings.Split(domain, ".")
last := len(ss) - 1
for i := 0; i < len(ss)/2; i++ {
ss[i], ss[last-i] = ss[last-i], ss[i]
}
return "/" + strings.Join(ss, "/")
}
// keyToDnsNameSplits used to reverse etcdv3 path to domain splits.
// e.g. /cloud/rancher/lb/sample => [sample lb rancher cloud]
func keyToDnsNameSplits(ss []string) {
for i := 0; i < len(ss)/2; i++ {
j := len(ss) - i - 1
ss[i], ss[j] = ss[j], ss[i]
}
}
// formatKey used to format a key as etcdv3 preferred
// e.g. 1.1.1.1 => 1_1_1_1
// e.g. sample.lb.rancher.cloud => sample_lb_rancher_cloud
func formatKey(key string) string {
return strings.Replace(key, ".", "_", -1)
}
/*
Copyright 2019 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package provider
import (
"context"
"encoding/json"
"fmt"
"strings"
"testing"
"github.com/coreos/etcd/clientv3"
"github.com/coreos/etcd/mvcc/mvccpb"
"github.com/kubernetes-incubator/external-dns/endpoint"
"github.com/kubernetes-incubator/external-dns/plan"
)
type fakeEtcdv3Client struct {
rs map[string]RDNSRecord
}
func (c fakeEtcdv3Client) Get(key string) ([]RDNSRecord, error) {
rs := make([]RDNSRecord, 0)
for k, v := range c.rs {
if strings.Contains(k, key) {
rs = append(rs, v)
}
}
return rs, nil
}
func (c fakeEtcdv3Client) List(rootDomain string) ([]RDNSRecord, error) {
var result []RDNSRecord
for key, value := range c.rs {
rootPath := rdnsPrefix + dnsNameToKey(rootDomain)
if strings.HasPrefix(key, rootPath) {
value.Key = key
result = append(result, value)
}
}
r := &clientv3.GetResponse{}
for _, v := range result {
b, err := json.Marshal(v)
if err != nil {
return nil, err
}
k := &mvccpb.KeyValue{
Key: []byte(v.Key),
Value: []byte(b),
}
r.Kvs = append(r.Kvs, k)
}
return c.aggregationRecords(r)
}
func (c fakeEtcdv3Client) Set(r RDNSRecord) error {
c.rs[r.Key] = r
return nil
}
func (c fakeEtcdv3Client) Delete(key string) error {
ks := make([]string, 0)
for k := range c.rs {
if strings.Contains(k, key) {
ks = append(ks, k)
}
}
for _, v := range ks {
delete(c.rs, v)
}
return nil
}
func TestARecordTranslation(t *testing.T) {
expectedTarget1 := "1.2.3.4"
expectedTarget2 := "2.3.4.5"
expectedDNSName := "p1xaf1.lb.rancher.cloud"
expectedRecordType := endpoint.RecordTypeA
client := fakeEtcdv3Client{
map[string]RDNSRecord{
"/rdnsv3/cloud/rancher/lb/p1xaf1/1_2_3_4": {Host: expectedTarget1},
"/rdnsv3/cloud/rancher/lb/p1xaf1/2_3_4_5": {Host: expectedTarget2},
},
}
provider := RDNSProvider{
client: client,
rootDomain: "lb.rancher.cloud",
}
endpoints, err := provider.Records()
if err != nil {
t.Fatal(err)
}
if len(endpoints) != 1 {
t.Fatalf("got unexpected number of endpoints: %d", len(endpoints))
}
if endpoints[0].DNSName != expectedDNSName {
t.Errorf("got unexpected DNS name: %s != %s", endpoints[0].DNSName, expectedDNSName)
}
if endpoints[0].Targets[0] != expectedTarget1 {
t.Errorf("got unexpected DNS target: %s != %s", endpoints[0].Targets[0], expectedTarget1)
}
if endpoints[0].Targets[1] != expectedTarget2 {
t.Errorf("got unexpected DNS target: %s != %s", endpoints[0].Targets[1], expectedTarget2)
}
if endpoints[0].RecordType != expectedRecordType {
t.Errorf("got unexpected DNS record type: %s != %s", endpoints[0].RecordType, expectedRecordType)
}
}
func TestTXTRecordTranslation(t *testing.T) {
expectedTarget := "string"
expectedDNSName := "p1xaf1.lb.rancher.cloud"
expectedRecordType := endpoint.RecordTypeTXT
client := fakeEtcdv3Client{
map[string]RDNSRecord{
"/rdnsv3/cloud/rancher/lb/p1xaf1": {Text: expectedTarget},
},
}
provider := RDNSProvider{
client: client,
rootDomain: "lb.rancher.cloud",
}
endpoints, err := provider.Records()
if err != nil {
t.Fatal(err)
}
if len(endpoints) != 1 {
t.Fatalf("got unexpected number of endpoints: %d", len(endpoints))
}
if endpoints[0].DNSName != expectedDNSName {
t.Errorf("got unexpected DNS name: %s != %s", endpoints[0].DNSName, expectedDNSName)
}
if endpoints[0].Targets[0] != expectedTarget {
t.Errorf("got unexpected DNS target: %s != %s", endpoints[0].Targets[0], expectedTarget)
}
if endpoints[0].RecordType != expectedRecordType {
t.Errorf("got unexpected DNS record type: %s != %s", endpoints[0].RecordType, expectedRecordType)
}
}
func TestAWithTXTRecordTranslation(t *testing.T) {
expectedTargets := map[string]string{
endpoint.RecordTypeA: "1.2.3.4",
endpoint.RecordTypeTXT: "string",
}
expectedDNSName := "p1xaf1.lb.rancher.cloud"
client := fakeEtcdv3Client{
map[string]RDNSRecord{
"/rdnsv3/cloud/rancher/lb/p1xaf1": {Host: "1.2.3.4", Text: "string"},
},
}
provider := RDNSProvider{
client: client,
rootDomain: "lb.rancher.cloud",
}
endpoints, err := provider.Records()
if err != nil {
t.Fatal(err)
}
if len(endpoints) != len(expectedTargets) {
t.Fatalf("got unexpected number of endpoints: %d", len(endpoints))
}
for _, ep := range endpoints {
expectedTarget := expectedTargets[ep.RecordType]
if expectedTarget == "" {
t.Errorf("got unexpected DNS record type: %s", ep.RecordType)
continue
}
delete(expectedTargets, ep.RecordType)
if ep.DNSName != expectedDNSName {
t.Errorf("got unexpected DNS name: %s != %s", ep.DNSName, expectedDNSName)
}
if ep.Targets[0] != expectedTarget {
t.Errorf("got unexpected DNS target: %s != %s", ep.Targets[0], expectedTarget)
}
}
}
func TestRDNSApplyChanges(t *testing.T) {
client := fakeEtcdv3Client{
map[string]RDNSRecord{},
}
provider := RDNSProvider{
client: client,
rootDomain: "lb.rancher.cloud",
}
changes1 := &plan.Changes{
Create: []*endpoint.Endpoint{
endpoint.NewEndpoint("p1xaf1.lb.rancher.cloud", endpoint.RecordTypeA, "5.5.5.5", "6.6.6.6"),
endpoint.NewEndpoint("p1xaf1.lb.rancher.cloud", endpoint.RecordTypeTXT, "string1"),
},
}
if err := provider.ApplyChanges(context.Background(), changes1); err != nil {
t.Error(err)
}
expectedRecords1 := map[string]RDNSRecord{
"/rdnsv3/cloud/rancher/lb/p1xaf1/5_5_5_5": {Host: "5.5.5.5", Text: "string1"},
"/rdnsv3/cloud/rancher/lb/p1xaf1/6_6_6_6": {Host: "6.6.6.6", Text: "string1"},
}
client.validateRecords(client.rs, expectedRecords1, t)
changes2 := &plan.Changes{
Create: []*endpoint.Endpoint{
endpoint.NewEndpoint("abx1v1.lb.rancher.cloud", endpoint.RecordTypeA, "7.7.7.7"),
},
UpdateNew: []*endpoint.Endpoint{
endpoint.NewEndpoint("p1xaf1.lb.rancher.cloud", endpoint.RecordTypeA, "8.8.8.8", "9.9.9.9"),
},
}
records, _ := provider.Records()
for _, ep := range records {
if ep.DNSName == "p1xaf1.lb.rancher.cloud" {
changes2.UpdateOld = append(changes2.UpdateOld, ep)
}
}
if err := provider.ApplyChanges(context.Background(), changes2); err != nil {
t.Error(err)
}
expectedRecords2 := map[string]RDNSRecord{
"/rdnsv3/cloud/rancher/lb/p1xaf1/8_8_8_8": {Host: "8.8.8.8"},
"/rdnsv3/cloud/rancher/lb/p1xaf1/9_9_9_9": {Host: "9.9.9.9"},
"/rdnsv3/cloud/rancher/lb/abx1v1/7_7_7_7": {Host: "7.7.7.7"},
}
client.validateRecords(client.rs, expectedRecords2, t)
changes3 := &plan.Changes{
Delete: []*endpoint.Endpoint{
endpoint.NewEndpoint("p1xaf1.lb.rancher.cloud", endpoint.RecordTypeA, "8.8.8.8", "9.9.9.9"),
},
}
if err := provider.ApplyChanges(context.Background(), changes3); err != nil {
t.Error(err)
}
expectedRecords3 := map[string]RDNSRecord{
"/rdnsv3/cloud/rancher/lb/abx1v1/7_7_7_7": {Host: "7.7.7.7"},
}
client.validateRecords(client.rs, expectedRecords3, t)
}
func (c fakeEtcdv3Client) aggregationRecords(result *clientv3.GetResponse) ([]RDNSRecord, error) {
var rs []RDNSRecord
bx := make(map[RDNSRecordType]RDNSRecord)
for _, n := range result.Kvs {
r := new(RDNSRecord)
if err := json.Unmarshal(n.Value, r); err != nil {
return nil, fmt.Errorf("%s: %s", n.Key, err.Error())
}
r.Key = string(n.Key)
if r.Host == "" && r.Text == "" {
continue
}
if r.Host != "" {
c := RDNSRecord{
AggregationHosts: r.AggregationHosts,
Host: r.Host,
Text: r.Text,
TTL: r.TTL,
Key: r.Key,
}
n, isContinue := appendRecords(c, endpoint.RecordTypeA, bx, rs)
if isContinue {
continue
}
rs = n
}
if r.Text != "" && r.Host == "" {
c := RDNSRecord{
AggregationHosts: []string{},
Host: r.Host,
Text: r.Text,
TTL: r.TTL,
Key: r.Key,
}
n, isContinue := appendRecords(c, endpoint.RecordTypeTXT, bx, rs)
if isContinue {
continue
}
rs = n
}
}
return rs, nil
}
func (c fakeEtcdv3Client) validateRecords(rs, expectedRs map[string]RDNSRecord, t *testing.T) {
if len(rs) != len(expectedRs) {
t.Errorf("wrong number of records: %d != %d", len(rs), len(expectedRs))
}
for key, value := range rs {
if _, ok := expectedRs[key]; !ok {
t.Errorf("unexpected record %s", key)
continue
}
expected := expectedRs[key]
delete(expectedRs, key)
if value.Host != expected.Host {
t.Errorf("wrong host for record %s: %s != %s", key, value.Host, expected.Host)
}
if value.Text != expected.Text {
t.Errorf("wrong text for record %s: %s != %s", key, value.Text, expected.Text)
}
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment