mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-06 01:26:59 +02:00
removes deprecated rdns provider
Signed-off-by: Raffaele Di Fazio <difazio.raffaele@gmail.com>
This commit is contained in:
parent
25b44e5c4e
commit
59fe374d79
@ -126,7 +126,6 @@ The following table clarifies the current status of the providers according to t
|
||||
| RFC2136 | Alpha | |
|
||||
| NS1 | Alpha | |
|
||||
| TransIP | Alpha | |
|
||||
| RancherDNS | Alpha | |
|
||||
| OVH | Alpha | |
|
||||
| Scaleway DNS | Alpha | @Sh4d1 |
|
||||
| UltraDNS | Alpha | |
|
||||
@ -188,7 +187,6 @@ The following tutorials are provided:
|
||||
* [OpenStack Designate](docs/tutorials/designate.md)
|
||||
* [Oracle Cloud Infrastructure (OCI) DNS](docs/tutorials/oracle.md)
|
||||
* [PowerDNS](docs/tutorials/pdns.md)
|
||||
* [RancherDNS (RDNS)](docs/tutorials/rdns.md)
|
||||
* [RFC2136](docs/tutorials/rfc2136.md)
|
||||
* [TransIP](docs/tutorials/transip.md)
|
||||
* [OVH](docs/tutorials/ovh.md)
|
||||
|
@ -1,173 +0,0 @@
|
||||
# RancherDNS
|
||||
|
||||
This tutorial describes how to setup ExternalDNS for usage within a kubernetes cluster that makes use of [RDNS](https://github.com/rancher/rdns-server) 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.k8s.io/external-dns/external-dns:v0.15.0
|
||||
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/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: external-dns
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["services","endpoints","pods"]
|
||||
verbs: ["get","watch","list"]
|
||||
- apiGroups: ["extensions","networking.k8s.io"]
|
||||
resources: ["ingresses"]
|
||||
verbs: ["get","watch","list"]
|
||||
- apiGroups: [""]
|
||||
resources: ["nodes"]
|
||||
verbs: ["list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
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.k8s.io/external-dns/external-dns:v0.15.0
|
||||
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: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: nginx
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
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#
|
||||
```
|
8
main.go
8
main.go
@ -65,7 +65,6 @@ import (
|
||||
"sigs.k8s.io/external-dns/provider/pdns"
|
||||
"sigs.k8s.io/external-dns/provider/pihole"
|
||||
"sigs.k8s.io/external-dns/provider/plural"
|
||||
"sigs.k8s.io/external-dns/provider/rdns"
|
||||
"sigs.k8s.io/external-dns/provider/rfc2136"
|
||||
"sigs.k8s.io/external-dns/provider/scaleway"
|
||||
"sigs.k8s.io/external-dns/provider/tencentcloud"
|
||||
@ -258,13 +257,6 @@ func main() {
|
||||
p, err = dnsimple.NewDnsimpleProvider(domainFilter, zoneIDFilter, cfg.DryRun)
|
||||
case "coredns", "skydns":
|
||||
p, err = coredns.NewCoreDNSProvider(domainFilter, cfg.CoreDNSPrefix, cfg.DryRun)
|
||||
case "rdns":
|
||||
p, err = rdns.NewRDNSProvider(
|
||||
rdns.RDNSConfig{
|
||||
DomainFilter: domainFilter,
|
||||
DryRun: cfg.DryRun,
|
||||
},
|
||||
)
|
||||
case "exoscale":
|
||||
p, err = exoscale.NewExoscaleProvider(
|
||||
cfg.ExoscaleAPIEnvironment,
|
||||
|
@ -445,7 +445,7 @@ func (cfg *Config) ParseFlags(args []string) error {
|
||||
app.Flag("nat64-networks", "Adding an A record for each AAAA record in NAT64-enabled networks; specify multiple times for multiple possible nets (optional)").StringsVar(&cfg.NAT64Networks)
|
||||
|
||||
// Flags related to providers
|
||||
providers := []string{"akamai", "alibabacloud", "aws", "aws-sd", "azure", "azure-dns", "azure-private-dns", "civo", "cloudflare", "coredns", "designate", "digitalocean", "dnsimple", "exoscale", "gandi", "godaddy", "google", "ibmcloud", "inmemory", "linode", "ns1", "oci", "ovh", "pdns", "pihole", "plural", "rdns", "rfc2136", "scaleway", "skydns", "tencentcloud", "transip", "ultradns", "webhook"}
|
||||
providers := []string{"akamai", "alibabacloud", "aws", "aws-sd", "azure", "azure-dns", "azure-private-dns", "civo", "cloudflare", "coredns", "designate", "digitalocean", "dnsimple", "exoscale", "gandi", "godaddy", "google", "ibmcloud", "inmemory", "linode", "ns1", "oci", "ovh", "pdns", "pihole", "plural", "rfc2136", "scaleway", "skydns", "tencentcloud", "transip", "ultradns", "webhook"}
|
||||
app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: "+strings.Join(providers, ", ")+")").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, providers...)
|
||||
app.Flag("provider-cache-time", "The time to cache the DNS provider record list requests.").Default(defaultConfig.ProviderCacheTime.String()).DurationVar(&cfg.ProviderCacheTime)
|
||||
app.Flag("domain-filter", "Limit possible target zones by a domain suffix; specify multiple times for multiple domains (optional)").Default("").StringsVar(&cfg.DomainFilter)
|
||||
|
@ -1,551 +0,0 @@
|
||||
/*
|
||||
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 rdns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
clientv3 "go.etcd.io/etcd/client/v3"
|
||||
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
"sigs.k8s.io/external-dns/plan"
|
||||
"sigs.k8s.io/external-dns/provider"
|
||||
)
|
||||
|
||||
const (
|
||||
etcdTimeout = 5 * time.Second
|
||||
rdnsMaxHosts = 10
|
||||
rdnsOriginalLabel = "originalText"
|
||||
rdnsPrefix = "/rdnsv3"
|
||||
rdnsTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
func init() {
|
||||
rand.New(rand.NewSource(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 endpoint.DomainFilter
|
||||
RootDomain string
|
||||
}
|
||||
|
||||
// RDNSProvider is an implementation of Provider for Rancher DNS(RDNS).
|
||||
type RDNSProvider struct {
|
||||
provider.BaseProvider
|
||||
client RDNSClient
|
||||
dryRun bool
|
||||
domainFilter endpoint.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(ctx context.Context) ([]*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: %w", err)
|
||||
}
|
||||
certificates = append(certificates, cert)
|
||||
}
|
||||
|
||||
config := &tls.Config{
|
||||
Certificates: certificates,
|
||||
InsecureSkipVerify: isInsecure,
|
||||
ServerName: name,
|
||||
}
|
||||
|
||||
if ca != "" {
|
||||
roots := x509.NewCertPool()
|
||||
pem, err := os.ReadFile(ca)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading %s: %w", ca, err)
|
||||
}
|
||||
ok := roots.AppendCertsFromPEM(pem)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("could not read root certs: %w", 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: %w", v.Key, err)
|
||||
}
|
||||
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: %w", n.Key, err)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
@ -1,355 +0,0 @@
|
||||
/*
|
||||
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 rdns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.etcd.io/etcd/api/v3/mvccpb"
|
||||
clientv3 "go.etcd.io/etcd/client/v3"
|
||||
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
"sigs.k8s.io/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: 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"
|
||||
expectedTargets := []string{expectedTarget1, expectedTarget2}
|
||||
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(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(endpoints) != 1 {
|
||||
t.Fatalf("got unexpected number of endpoints: %d", len(endpoints))
|
||||
}
|
||||
|
||||
ep := endpoints[0]
|
||||
if ep.DNSName != expectedDNSName {
|
||||
t.Errorf("got unexpected DNS name: %s != %s", ep.DNSName, expectedDNSName)
|
||||
}
|
||||
assert.Contains(t, expectedTargets, ep.Targets[0])
|
||||
assert.Contains(t, expectedTargets, ep.Targets[1])
|
||||
if ep.RecordType != expectedRecordType {
|
||||
t.Errorf("got unexpected DNS record type: %s != %s", ep.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(context.Background())
|
||||
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(context.Background())
|
||||
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(context.Background())
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user