diff --git a/README.md b/README.md index 6ef0eeb14c..6ab5602b3c 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/docs/tutorials/rdns.md b/docs/tutorials/rdns.md deleted file mode 100644 index 7e10403c1c..0000000000 --- a/docs/tutorials/rdns.md +++ /dev/null @@ -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# -``` diff --git a/main.go b/main.go index 619d25fcee..d5112ccd12 100644 --- a/main.go +++ b/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, diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index 9155bccd3f..af4b87de3e 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -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) diff --git a/provider/rdns/rdns.go b/provider/rdns/rdns.go deleted file mode 100644 index 10766c8765..0000000000 --- a/provider/rdns/rdns.go +++ /dev/null @@ -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) -} diff --git a/provider/rdns/rdns_test.go b/provider/rdns/rdns_test.go deleted file mode 100644 index 99358f4ce3..0000000000 --- a/provider/rdns/rdns_test.go +++ /dev/null @@ -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) - } - } -}