Skip to content
This repository was archived by the owner on Jun 5, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM golang:1.24-bookworm AS builder
FROM golang:1.25-bookworm AS builder

COPY . /code/external-dns-coredns-webhook
WORKDIR /code/external-dns-coredns-webhook
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# ExternalDNS Plugin CoreDNS Webhook

## Deprecated

With the release of version 0.21 or newer, every function is now included inside the in-tree provider.

## Commandline

```
Expand Down
170 changes: 87 additions & 83 deletions coredns.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,24 +28,23 @@ import (
"time"

log "github.com/sirupsen/logrus"
"go.etcd.io/etcd/api/v3/mvccpb"
etcdcv3 "go.etcd.io/etcd/client/v3"

"sigs.k8s.io/external-dns/pkg/tlsutils"

"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
)

func init() {
rand.Seed(time.Now().UnixNano())
}

const (
priority = 10 // default priority when nothing is set
etcdTimeout = 5 * time.Second

randomPrefixLabel = "prefix"
providerSpecificGroup = "webhook/coredns-group"
randomPrefixLabel = "prefix"
providerSpecificGroup = "webhook/coredns-group"
providerSpecificGroup2 = "coredns/group"
)

type CoreDNSConfig struct {
Expand All @@ -65,6 +64,7 @@ type coreDNSProvider struct {
client coreDNSClient
dryRun bool
CoreDNSConfig
strictlyOwned bool
}

// Service represents CoreDNS etcd record
Expand Down Expand Up @@ -92,19 +92,19 @@ type Service struct {
// Etcd key where we found this service and ignored from json un-/marshaling
Key string `json:"-"`

// ManagedBy is used to prevent service to be added by different external-dns (only used by external-dns)
ManagedBy string `json:"managedby,omitempty"`
// Owner is used to prevent service to be added by different external-dns (only used by external-dns)
Owner string `json:"owner,omitempty"`
}

type etcdClient struct {
client *etcdcv3.Client
managedBy string
ignoreEmptyManagedBy bool
client *etcdcv3.Client
owner string
strictlyOwned bool
}

var _ coreDNSClient = etcdClient{}

// GetServices return all Service records stored in etcd stored anywhere under the given key (recursively)
// GetServices GetService return all Service records stored in etcd stored anywhere under the given key (recursively)
Comment thread
farodin91 marked this conversation as resolved.
func (c etcdClient) GetServices(ctx context.Context, prefix string) ([]*Service, error) {
ctx, cancel := context.WithTimeout(ctx, etcdTimeout)
defer cancel()
Expand All @@ -118,31 +118,26 @@ func (c etcdClient) GetServices(ctx context.Context, prefix string) ([]*Service,
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: %w", n.Key, err)
svc, err := c.unmarshalService(n)
if err != nil {
return nil, err
}
if c.strictlyOwned && svc.Owner != c.owner {
continue
}
b := Service{
Host: svc.Host,
Port: svc.Port,
Priority: svc.Priority,
Weight: svc.Weight,
Text: svc.Text,
Key: string(n.Key),
ManagedBy: svc.ManagedBy,
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
}
if c.managedBy != "" {
if c.ignoreEmptyManagedBy && b.ManagedBy != c.managedBy {
continue
} else if !c.ignoreEmptyManagedBy && b.ManagedBy != "" && b.ManagedBy != c.managedBy {
continue
}
}
bx[b] = true

svc.Key = string(n.Key)
Expand All @@ -151,7 +146,6 @@ func (c etcdClient) GetServices(ctx context.Context, prefix string) ([]*Service,
}
svcs = append(svcs, svc)
}

return svcs, nil
}

Expand All @@ -160,13 +154,23 @@ func (c etcdClient) SaveService(ctx context.Context, service *Service) error {
ctx, cancel := context.WithTimeout(ctx, etcdTimeout)
defer cancel()

if c.managedBy != "" {
service.ManagedBy = c.managedBy
}
if ownedBy, err := c.IsOwnedBy(ctx, service.Key); err != nil {
return err
} else if !ownedBy {
return fmt.Errorf("key %q is not owned by this service", service.Key)
// check only for empty OwnedBy
if c.strictlyOwned && service.Owner != c.owner {
r, err := c.client.Get(ctx, service.Key)
if err != nil {
return fmt.Errorf("etcd get %q: %w", service.Key, err)
}
// Key missing -> treat as owned (safe to create)
if r != nil && len(r.Kvs) != 0 {
svc, err := c.unmarshalService(r.Kvs[0])
if err != nil {
return fmt.Errorf("failed to unmarshal value for key %q: %w", service.Key, err)
}
if svc.Owner != c.owner {
return fmt.Errorf("key %q is not owned by this provider", service.Key)
}
}
service.Owner = c.owner
}

value, err := json.Marshal(&service)
Expand All @@ -180,54 +184,43 @@ func (c etcdClient) SaveService(ctx context.Context, service *Service) error {
return nil
}

func (c etcdClient) IsOwnedBy(ctx context.Context, key string) (bool, error) {
// DeleteService deletes service record from etcd
func (c etcdClient) DeleteService(ctx context.Context, key string) error {
ctx, cancel := context.WithTimeout(ctx, etcdTimeout)
defer cancel()

if c.managedBy == "" {
return true, nil
}

r, err := c.client.Get(ctx, key)
if err != nil {
return false, err
}
if r == nil {
return true, nil
} else if len(r.Kvs) > 1 {
return false, fmt.Errorf("found multiple keys with the same key this service")
} else if len(r.Kvs) == 0 {
return true, nil
}
for _, n := range r.Kvs {
svc := new(Service)
if err := json.Unmarshal(n.Value, svc); err != nil {
return false, fmt.Errorf("%s: %w", n.Key, err)
if c.strictlyOwned {
rs, err := c.client.Get(ctx, key, etcdcv3.WithPrefix())
if err != nil {
return err
}
for _, r := range rs.Kvs {
svc, err := c.unmarshalService(r)
if err != nil {
return err
}
if svc.Owner != c.owner {
continue
}

if !c.ignoreEmptyManagedBy && svc.ManagedBy == "" {
return true, nil
}
if svc.ManagedBy == c.managedBy {
return true, nil
_, err = c.client.Delete(ctx, string(r.Key))
if err != nil {
return err
}
}
return err
} else {
_, err := c.client.Delete(ctx, key, etcdcv3.WithPrefix())
return err
}
return false, nil
}

// DeleteService deletes service record from etcd
func (c etcdClient) DeleteService(ctx context.Context, key string) error {
ctx, cancel := context.WithTimeout(ctx, etcdTimeout)
defer cancel()

if owned, err := c.IsOwnedBy(ctx, key); err != nil {
return err
} else if !owned {
return fmt.Errorf("key %q is not owned by this service", key)
func (c etcdClient) unmarshalService(n *mvccpb.KeyValue) (*Service, error) {
svc := new(Service)
if err := json.Unmarshal(n.Value, svc); err != nil {
return nil, fmt.Errorf("failed to unmarshal %q: %w", n.Key, err)
}

_, err := c.client.Delete(ctx, key, etcdcv3.WithPrefix())
return err
return svc, nil
}

// builds etcd client config depending on connection scheme and TLS parameters
Expand All @@ -240,9 +233,10 @@ func getETCDConfig() (*etcdcv3.Config, error) {
firstURL := strings.ToLower(etcdURLs[0])
etcdUsername := os.Getenv("ETCD_USERNAME")
etcdPassword := os.Getenv("ETCD_PASSWORD")
if strings.HasPrefix(firstURL, "http://") {
switch {
case strings.HasPrefix(firstURL, "http://"):
return &etcdcv3.Config{Endpoints: etcdURLs, Username: etcdUsername, Password: etcdPassword}, nil
} else if strings.HasPrefix(firstURL, "https://") {
case strings.HasPrefix(firstURL, "https://"):
tlsConfig, err := tlsutils.CreateTLSConfig("ETCD")
if err != nil {
return nil, err
Expand All @@ -254,13 +248,13 @@ func getETCDConfig() (*etcdcv3.Config, error) {
Username: etcdUsername,
Password: etcdPassword,
}, nil
} else {
default:
return nil, errors.New("etcd URLs must start with either http:// or https://")
}
}

// the newETCDClient is an etcd client constructor
func newETCDClient(managedBy string, ignoreEmptyManagedBy bool) (coreDNSClient, error) {
func newETCDClient(owner string, strictlyOwned bool) (coreDNSClient, error) {
cfg, err := getETCDConfig()
if err != nil {
return nil, err
Expand All @@ -269,12 +263,12 @@ func newETCDClient(managedBy string, ignoreEmptyManagedBy bool) (coreDNSClient,
if err != nil {
return nil, err
}
return etcdClient{c, managedBy, ignoreEmptyManagedBy}, nil
return etcdClient{c, owner, strictlyOwned}, nil
}

// NewCoreDNSProvider is a CoreDNS provider constructor
func NewCoreDNSProvider(config CoreDNSConfig, managedBy string, ignoreEmptyManagedBy, dryRun bool) (provider.Provider, error) {
client, err := newETCDClient(managedBy, ignoreEmptyManagedBy)
func NewCoreDNSProvider(config CoreDNSConfig, owner string, strictlyOwned, dryRun bool) (provider.Provider, error) {
client, err := newETCDClient(owner, strictlyOwned)
if err != nil {
return nil, err
}
Expand All @@ -283,6 +277,7 @@ func NewCoreDNSProvider(config CoreDNSConfig, managedBy string, ignoreEmptyManag
client: client,
dryRun: dryRun,
CoreDNSConfig: config,
strictlyOwned: strictlyOwned,
}, nil
}

Expand Down Expand Up @@ -339,9 +334,13 @@ func (p coreDNSProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, err
)
if service.Group != "" {
ep.WithProviderSpecific(providerSpecificGroup, service.Group)
ep.WithProviderSpecific(providerSpecificGroup2, service.Group)
}
log.Debugf("Creating new ep (%s) with new service host (%s)", ep, service.Host)
}
if p.strictlyOwned {
ep.Labels[endpoint.OwnerLabelKey] = service.Owner
}
ep.Labels["originalText"] = service.Text
ep.Labels[randomPrefixLabel] = prefix
ep.Labels[service.Host] = prefix
Expand All @@ -353,6 +352,9 @@ func (p coreDNSProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, err
endpoint.RecordTypeTXT,
service.Text,
)
if p.strictlyOwned {
ep.Labels[endpoint.OwnerLabelKey] = service.Owner
}
ep.Labels[randomPrefixLabel] = prefix
result = append(result, ep)
}
Expand Down Expand Up @@ -426,11 +428,13 @@ func (p coreDNSProvider) createServicesForEndpoint(ctx context.Context, dnsName
prefix = fmt.Sprintf("%08x", rand.Int31())
log.Infof("Generating new prefix: (%s)", prefix)
}

group := ""
if prop, ok := ep.GetProviderSpecificProperty(providerSpecificGroup); ok {
group = prop
}
if prop, ok := ep.GetProviderSpecificProperty(providerSpecificGroup2); ok {
group = prop
}
service := Service{
Host: target,
Text: ep.Labels["originalText"],
Expand Down
Loading
Loading