mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-06 09:36:58 +02:00
Update CoreDNS provider to use etcd v3 client
This commit is contained in:
parent
21b7c3a613
commit
c668865e65
@ -17,7 +17,7 @@ limitations under the License.
|
||||
package provider
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
@ -26,14 +26,12 @@ import (
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
etcd "github.com/coreos/etcd/client"
|
||||
etcdcv3 "github.com/coreos/etcd/clientv3"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/kubernetes-incubator/external-dns/endpoint"
|
||||
"github.com/kubernetes-incubator/external-dns/plan"
|
||||
@ -43,8 +41,17 @@ func init() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
// skyDNSClient is an interface to work with SkyDNS service records in etcd
|
||||
type skyDNSClient interface {
|
||||
const (
|
||||
priority = 10 // default priority when nothing is set
|
||||
etcdTimeout = 5 * time.Second
|
||||
|
||||
coreDNSPrefix = "/skydns/"
|
||||
|
||||
randomPrefixLabel = "prefix"
|
||||
)
|
||||
|
||||
// coreDNSClient is an interface to work with CoreDNS service records in etcd
|
||||
type coreDNSClient interface {
|
||||
GetServices(prefix string) ([]*Service, error)
|
||||
SaveService(value *Service) error
|
||||
DeleteService(key string) error
|
||||
@ -53,10 +60,10 @@ type skyDNSClient interface {
|
||||
type coreDNSProvider struct {
|
||||
dryRun bool
|
||||
domainFilter DomainFilter
|
||||
client skyDNSClient
|
||||
client coreDNSClient
|
||||
}
|
||||
|
||||
// Service represents SkyDNS/CoreDNS etcd record
|
||||
// Service represents CoreDNS etcd record
|
||||
type Service struct {
|
||||
Host string `json:"host,omitempty"`
|
||||
Port int `json:"port,omitempty"`
|
||||
@ -83,52 +90,58 @@ type Service struct {
|
||||
}
|
||||
|
||||
type etcdClient struct {
|
||||
api etcd.KeysAPI
|
||||
client *etcdcv3.Client
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
var _ skyDNSClient = etcdClient{}
|
||||
var _ coreDNSClient = etcdClient{}
|
||||
|
||||
// GetService return all Service records stored in etcd stored anywhere under the given key (recursively)
|
||||
func (c etcdClient) GetServices(prefix string) ([]*Service, error) {
|
||||
var result []*Service
|
||||
opts := &etcd.GetOptions{Recursive: true}
|
||||
data, err := c.api.Get(context.Background(), prefix, opts)
|
||||
ctx, cancel := context.WithTimeout(c.ctx, etcdTimeout)
|
||||
defer cancel()
|
||||
|
||||
path := prefix
|
||||
r, err := c.client.Get(ctx, path, etcdcv3.WithPrefix())
|
||||
if err != nil {
|
||||
if etcd.IsKeyNotFound(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
queue := list.New()
|
||||
queue.PushFront(data.Node)
|
||||
for queueNode := queue.Front(); queueNode != nil; queueNode = queueNode.Next() {
|
||||
node := queueNode.Value.(*etcd.Node)
|
||||
if node.Dir {
|
||||
for _, childNode := range node.Nodes {
|
||||
queue.PushBack(childNode)
|
||||
}
|
||||
var svcs []*Service
|
||||
bx := make(map[Service]bool)
|
||||
for _, n := range r.Kvs {
|
||||
svc := new(Service)
|
||||
if err := json.Unmarshal(n.Value, svc); err != nil {
|
||||
return nil, fmt.Errorf("%s: %s", n.Key, err.Error())
|
||||
}
|
||||
b := Service{Host: svc.Host, Port: svc.Port, Priority: svc.Priority, Weight: svc.Weight, Text: svc.Text, Key: string(n.Key)}
|
||||
if _, ok := bx[b]; ok {
|
||||
// skip the service if already added to service list.
|
||||
// the same service might be found in multiple etcd nodes.
|
||||
continue
|
||||
}
|
||||
service := &Service{}
|
||||
err = json.Unmarshal([]byte(node.Value), service)
|
||||
if err != nil {
|
||||
log.Error("Cannot parse JSON value ", node.Value)
|
||||
continue
|
||||
bx[b] = true
|
||||
|
||||
svc.Key = string(n.Key)
|
||||
if svc.Priority == 0 {
|
||||
svc.Priority = priority
|
||||
}
|
||||
service.Key = node.Key
|
||||
result = append(result, service)
|
||||
svcs = append(svcs, svc)
|
||||
}
|
||||
return result, nil
|
||||
|
||||
return svcs, nil
|
||||
}
|
||||
|
||||
// SaveService persists service data into etcd
|
||||
func (c etcdClient) SaveService(service *Service) error {
|
||||
ctx, cancel := context.WithTimeout(c.ctx, etcdTimeout)
|
||||
defer cancel()
|
||||
|
||||
value, err := json.Marshal(&service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = c.api.Set(context.Background(), service.Key, string(value), nil)
|
||||
_, err = c.client.Put(ctx, service.Key, string(value))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -137,9 +150,11 @@ func (c etcdClient) SaveService(service *Service) error {
|
||||
|
||||
// DeleteService deletes service record from etcd
|
||||
func (c etcdClient) DeleteService(key string) error {
|
||||
_, err := c.api.Delete(context.Background(), key, nil)
|
||||
return err
|
||||
ctx, cancel := context.WithTimeout(c.ctx, etcdTimeout)
|
||||
defer cancel()
|
||||
|
||||
_, err := c.client.Delete(ctx, key)
|
||||
return err
|
||||
}
|
||||
|
||||
// loads TLS artifacts and builds tls.Clonfig object
|
||||
@ -186,21 +201,8 @@ func loadRoots(caPath string) (*x509.CertPool, error) {
|
||||
return roots, nil
|
||||
}
|
||||
|
||||
// constructs http.Transport object for https protocol
|
||||
func newHTTPSTransport(cc *tls.Config) *http.Transport {
|
||||
return &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).Dial,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
TLSClientConfig: cc,
|
||||
}
|
||||
}
|
||||
|
||||
// builds etcd client config depending on connection scheme and TLS parameters
|
||||
func getETCDConfig() (*etcd.Config, error) {
|
||||
func getETCDConfig() (*etcdcv3.Config, error) {
|
||||
etcdURLsStr := os.Getenv("ETCD_URLS")
|
||||
if etcdURLsStr == "" {
|
||||
etcdURLsStr = "http://localhost:2379"
|
||||
@ -208,7 +210,7 @@ func getETCDConfig() (*etcd.Config, error) {
|
||||
etcdURLs := strings.Split(etcdURLsStr, ",")
|
||||
firstURL := strings.ToLower(etcdURLs[0])
|
||||
if strings.HasPrefix(firstURL, "http://") {
|
||||
return &etcd.Config{Endpoints: etcdURLs}, nil
|
||||
return &etcdcv3.Config{Endpoints: etcdURLs}, nil
|
||||
} else if strings.HasPrefix(firstURL, "https://") {
|
||||
caFile := os.Getenv("ETCD_CA_FILE")
|
||||
certFile := os.Getenv("ETCD_CERT_FILE")
|
||||
@ -220,9 +222,9 @@ func getETCDConfig() (*etcd.Config, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &etcd.Config{
|
||||
return &etcdcv3.Config{
|
||||
Endpoints: etcdURLs,
|
||||
Transport: newHTTPSTransport(tlsConfig),
|
||||
TLS: tlsConfig,
|
||||
}, nil
|
||||
} else {
|
||||
return nil, errors.New("etcd URLs must start with either http:// or https://")
|
||||
@ -230,16 +232,16 @@ func getETCDConfig() (*etcd.Config, error) {
|
||||
}
|
||||
|
||||
//newETCDClient is an etcd client constructor
|
||||
func newETCDClient() (skyDNSClient, error) {
|
||||
func newETCDClient() (coreDNSClient, error) {
|
||||
cfg, err := getETCDConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c, err := etcd.New(*cfg)
|
||||
c, err := etcdcv3.New(*cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return etcdClient{etcd.NewKeysAPI(c)}, nil
|
||||
return etcdClient{c, context.Background()}, nil
|
||||
}
|
||||
|
||||
// NewCoreDNSProvider is a CoreDNS provider constructor
|
||||
@ -255,16 +257,16 @@ func NewCoreDNSProvider(domainFilter DomainFilter, dryRun bool) (Provider, error
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Records returns all DNS records found in SkyDNS/CoreDNS etcd backend. Depending on the record fields
|
||||
// Records returns all DNS records found in CoreDNS etcd backend. Depending on the record fields
|
||||
// it may be mapped to one or two records of type A, CNAME, TXT, A+TXT, CNAME+TXT
|
||||
func (p coreDNSProvider) Records() ([]*endpoint.Endpoint, error) {
|
||||
var result []*endpoint.Endpoint
|
||||
services, err := p.client.GetServices("/skydns")
|
||||
services, err := p.client.GetServices(coreDNSPrefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, service := range services {
|
||||
domains := strings.Split(strings.TrimPrefix(service.Key, "/skydns/"), "/")
|
||||
domains := strings.Split(strings.TrimPrefix(service.Key, coreDNSPrefix), "/")
|
||||
reverse(domains)
|
||||
dnsName := strings.Join(domains[service.TargetStrip:], ".")
|
||||
if !p.domainFilter.Match(dnsName) {
|
||||
@ -272,13 +274,14 @@ func (p coreDNSProvider) Records() ([]*endpoint.Endpoint, error) {
|
||||
}
|
||||
prefix := strings.Join(domains[:service.TargetStrip], ".")
|
||||
if service.Host != "" {
|
||||
ep := endpoint.NewEndpoint(
|
||||
ep := endpoint.NewEndpointWithTTL(
|
||||
dnsName,
|
||||
guessRecordType(service.Host),
|
||||
endpoint.TTL(service.TTL),
|
||||
service.Host,
|
||||
)
|
||||
ep.Labels["originalText"] = service.Text
|
||||
ep.Labels["prefix"] = prefix
|
||||
ep.Labels[randomPrefixLabel] = prefix
|
||||
result = append(result, ep)
|
||||
}
|
||||
if service.Text != "" {
|
||||
@ -287,20 +290,21 @@ func (p coreDNSProvider) Records() ([]*endpoint.Endpoint, error) {
|
||||
endpoint.RecordTypeTXT,
|
||||
service.Text,
|
||||
)
|
||||
ep.Labels["prefix"] = prefix
|
||||
ep.Labels[randomPrefixLabel] = prefix
|
||||
result = append(result, ep)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ApplyChanges stores changes back to etcd converting them to SkyDNS format and aggregating A/CNAME and TXT records
|
||||
// ApplyChanges stores changes back to etcd converting them to CoreDNS format and aggregating A/CNAME and TXT records
|
||||
func (p coreDNSProvider) ApplyChanges(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 {
|
||||
for i, ep := range changes.UpdateNew {
|
||||
ep.Labels[randomPrefixLabel] = changes.UpdateOld[i].Labels[randomPrefixLabel]
|
||||
grouped[ep.DNSName] = append(grouped[ep.DNSName], ep)
|
||||
}
|
||||
for dnsName, group := range grouped {
|
||||
@ -313,7 +317,7 @@ func (p coreDNSProvider) ApplyChanges(changes *plan.Changes) error {
|
||||
if ep.RecordType == endpoint.RecordTypeTXT {
|
||||
continue
|
||||
}
|
||||
prefix := ep.Labels["prefix"]
|
||||
prefix := ep.Labels[randomPrefixLabel]
|
||||
if prefix == "" {
|
||||
prefix = fmt.Sprintf("%08x", rand.Int31())
|
||||
}
|
||||
@ -322,6 +326,7 @@ func (p coreDNSProvider) ApplyChanges(changes *plan.Changes) error {
|
||||
Text: ep.Labels["originalText"],
|
||||
Key: etcdKeyFor(prefix + "." + dnsName),
|
||||
TargetStrip: strings.Count(prefix, ".") + 1,
|
||||
TTL: uint32(ep.RecordTTL),
|
||||
}
|
||||
services = append(services, service)
|
||||
}
|
||||
@ -331,13 +336,14 @@ func (p coreDNSProvider) ApplyChanges(changes *plan.Changes) error {
|
||||
continue
|
||||
}
|
||||
if index >= len(services) {
|
||||
prefix := ep.Labels["prefix"]
|
||||
prefix := ep.Labels[randomPrefixLabel]
|
||||
if prefix == "" {
|
||||
prefix = fmt.Sprintf("%08x", rand.Int31())
|
||||
}
|
||||
services = append(services, Service{
|
||||
Key: etcdKeyFor(prefix + "." + dnsName),
|
||||
TargetStrip: strings.Count(prefix, ".") + 1,
|
||||
TTL: uint32(ep.RecordTTL),
|
||||
})
|
||||
}
|
||||
services[index].Text = ep.Targets[0]
|
||||
@ -349,7 +355,7 @@ func (p coreDNSProvider) ApplyChanges(changes *plan.Changes) error {
|
||||
}
|
||||
|
||||
for _, service := range services {
|
||||
log.Infof("Add/set key %s to Host=%s, Text=%s", service.Key, service.Host, service.Text)
|
||||
log.Infof("Add/set key %s to Host=%s, Text=%s, TTL=%d", service.Key, service.Host, service.Text, service.TTL)
|
||||
if !p.dryRun {
|
||||
err := p.client.SaveService(&service)
|
||||
if err != nil {
|
||||
@ -361,8 +367,8 @@ func (p coreDNSProvider) ApplyChanges(changes *plan.Changes) error {
|
||||
|
||||
for _, ep := range changes.Delete {
|
||||
dnsName := ep.DNSName
|
||||
if ep.Labels["prefix"] != "" {
|
||||
dnsName = ep.Labels["prefix"] + "." + dnsName
|
||||
if ep.Labels[randomPrefixLabel] != "" {
|
||||
dnsName = ep.Labels[randomPrefixLabel] + "." + dnsName
|
||||
}
|
||||
key := etcdKeyFor(dnsName)
|
||||
log.Infof("Delete key %s", key)
|
||||
@ -387,7 +393,7 @@ func guessRecordType(target string) string {
|
||||
func etcdKeyFor(dnsName string) string {
|
||||
domains := strings.Split(dnsName, ".")
|
||||
reverse(domains)
|
||||
return "/skydns/" + strings.Join(domains, "/")
|
||||
return coreDNSPrefix + strings.Join(domains, "/")
|
||||
}
|
||||
|
||||
func reverse(slice []string) {
|
||||
|
@ -235,8 +235,6 @@ func TestCoreDNSApplyChanges(t *testing.T) {
|
||||
}
|
||||
validateServices(client.services, expectedServices1, t, 1)
|
||||
|
||||
updatedEp := endpoint.NewEndpoint("domain1.local", endpoint.RecordTypeA, "6.6.6.6")
|
||||
updatedEp.Labels["originalText"] = "string1"
|
||||
changes2 := &plan.Changes{
|
||||
Create: []*endpoint.Endpoint{
|
||||
endpoint.NewEndpoint("domain3.local", endpoint.RecordTypeA, "7.7.7.7"),
|
||||
@ -245,6 +243,12 @@ func TestCoreDNSApplyChanges(t *testing.T) {
|
||||
endpoint.NewEndpoint("domain1.local", "A", "6.6.6.6"),
|
||||
},
|
||||
}
|
||||
records, _ := coredns.Records()
|
||||
for _, ep := range records {
|
||||
if ep.DNSName == "domain1.local" {
|
||||
changes2.UpdateOld = append(changes2.UpdateOld, ep)
|
||||
}
|
||||
}
|
||||
applyServiceChanges(coredns, changes2)
|
||||
|
||||
expectedServices2 := map[string]*Service{
|
||||
|
Loading…
Reference in New Issue
Block a user