Skip to content

Commit 8085ce2

Browse files
authored
feat(coredns): use txt-owner-id to strictly separated external-dns instances (#5921)
* feat(coredns): use managed-by to separate records Signed-off-by: Jan Jansen <jan.jansen@gdata.de> * feat(coredns): use txt-owner-id to strictly separated external-dns instances Signed-off-by: Jan Jansen <jan.jansen@gdata.de> * fix tests Signed-off-by: Jan Jansen <jan.jansen@gdata.de> * fix reviewer comments Signed-off-by: Jan Jansen <jan.jansen@gdata.de> * answer review comments * fix deletion behavior and remove extra function * fix markdown * fix tests again --------- Signed-off-by: Jan Jansen <jan.jansen@gdata.de>
1 parent 00e04e9 commit 8085ce2

File tree

7 files changed

+618
-66
lines changed

7 files changed

+618
-66
lines changed

controller/execute.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ func buildProvider(
248248
case "dnsimple":
249249
p, err = dnsimple.NewDnsimpleProvider(domainFilter, zoneIDFilter, cfg.DryRun)
250250
case "coredns", "skydns":
251-
p, err = coredns.NewCoreDNSProvider(domainFilter, cfg.CoreDNSPrefix, cfg.DryRun)
251+
p, err = coredns.NewCoreDNSProvider(domainFilter, cfg.CoreDNSPrefix, cfg.TXTOwnerID, cfg.CoreDNSStrictlyOwned, cfg.DryRun)
252252
case "exoscale":
253253
p, err = exoscale.NewExoscaleProvider(
254254
cfg.ExoscaleAPIEnvironment,

docs/flags.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@
9797
| `--cloudflare-region-key=""` | When using the Cloudflare provider, specify the default region for Regional Services. Any value other than an empty string will enable the Regional Services feature (optional) |
9898
| `--cloudflare-record-comment=""` | When using the Cloudflare provider, specify the comment for the DNS records (default: '') |
9999
| `--coredns-prefix="/skydns/"` | When using the CoreDNS provider, specify the prefix name |
100+
| `--[no-]coredns-strictly-owned` | When using the CoreDNS provider, store and filter strictly by txt-owner-id using an extra field inside of the etcd service (default: false) |
100101
| `--akamai-serviceconsumerdomain=""` | When using the Akamai provider, specify the base URL (required when --provider=akamai and edgerc-path not specified) |
101102
| `--akamai-client-token=""` | When using the Akamai provider, specify the client token (required when --provider=akamai and edgerc-path not specified) |
102103
| `--akamai-client-secret=""` | When using the Akamai provider, specify the client secret (required when --provider=akamai and edgerc-path not specified) |

docs/tutorials/coredns.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,47 @@
22

33
- [Documentation](https://coredns.io/)
44

5+
## Multi cluster support options
6+
7+
The CoreDNS provider allows records from different CoreDNS providers to be separated in a single etcd
8+
by activating the setting `--coredns-strictly-owned` flag and set `txt-owner-id`. It will prevent any
9+
override (update/create/delete) of records by a different owner and prevent loading of records by a
10+
different owner.
11+
12+
Flow:
13+
14+
```mermaid
15+
graph TD
16+
subgraph ETCD
17+
store--> E(services from Cluster A)
18+
store--> F(services from Cluster B)
19+
store--> G(services from someone else)
20+
end
21+
subgraph Cluster A
22+
A(external-dns with stictly-owned)
23+
end
24+
A --> E
25+
subgraph Cluster B
26+
B(external-dns with stictly-owned)
27+
end
28+
B --> F
29+
store --> CoreDNS
30+
```
31+
32+
This features works directly without any change to CoreDNS. CoreDNS will ignore this field inside the etcd record.
33+
34+
### Other entries inside etcd
35+
36+
Service entries in etcd without an `ownedby` field will be filtered out by the provider if `strictly-owned` is activated.
37+
Warning: If you activate `strictly-owned` afterwards, these entries will be ignored as the `ownedby` field is empty.
38+
39+
### Ways to migrate to a multi cluster setup
40+
41+
Ways:
42+
43+
1. Add the correct owner to all services inside etcd by adding the field `ownedby` to the JSON.
44+
2. Remove all services and allow them to be required again after restarting the provider. (Possible downtime.)
45+
546
## Specific service annotation options
647

748
### Groups

endpoint/endpoint_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -583,7 +583,7 @@ func TestIsOwnedBy(t *testing.T) {
583583
Labels: tt.fields.Labels,
584584
}
585585
if got := e.IsOwnedBy(tt.args.ownerID); got != tt.want {
586-
t.Errorf("Endpoint.IsOwnedBy() = %v, want %v", got, tt.want)
586+
t.Errorf("Endpoint.isOwnedBy() = %v, want %v", got, tt.want)
587587
}
588588
})
589589
}

pkg/apis/externaldns/types.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ type Config struct {
119119
CloudflareRegionalServices bool
120120
CloudflareRegionKey string
121121
CoreDNSPrefix string
122+
CoreDNSStrictlyOwned bool
122123
AkamaiServiceConsumerDomain string
123124
AkamaiClientToken string
124125
AkamaiClientSecret string
@@ -269,6 +270,7 @@ var defaultConfig = &Config{
269270
Compatibility: "",
270271
ConnectorSourceServer: "localhost:8080",
271272
CoreDNSPrefix: "/skydns/",
273+
CoreDNSStrictlyOwned: false,
272274
CRDSourceAPIVersion: "externaldns.k8s.io/v1alpha1",
273275
CRDSourceKind: "DNSEndpoint",
274276
DefaultTargets: []string{},
@@ -702,6 +704,7 @@ func bindFlags(b FlagBinder, cfg *Config) {
702704
b.StringVar("cloudflare-record-comment", "When using the Cloudflare provider, specify the comment for the DNS records (default: '')", "", &cfg.CloudflareDNSRecordsComment)
703705

704706
b.StringVar("coredns-prefix", "When using the CoreDNS provider, specify the prefix name", defaultConfig.CoreDNSPrefix, &cfg.CoreDNSPrefix)
707+
b.BoolVar("coredns-strictly-owned", "When using the CoreDNS provider, store and filter strictly by txt-owner-id using an extra field inside of the etcd service (default: false)", defaultConfig.CoreDNSStrictlyOwned, &cfg.CoreDNSStrictlyOwned)
705708
b.StringVar("akamai-serviceconsumerdomain", "When using the Akamai provider, specify the base URL (required when --provider=akamai and edgerc-path not specified)", defaultConfig.AkamaiServiceConsumerDomain, &cfg.AkamaiServiceConsumerDomain)
706709
b.StringVar("akamai-client-token", "When using the Akamai provider, specify the client token (required when --provider=akamai and edgerc-path not specified)", defaultConfig.AkamaiClientToken, &cfg.AkamaiClientToken)
707710
b.StringVar("akamai-client-secret", "When using the Akamai provider, specify the client secret (required when --provider=akamai and edgerc-path not specified)", defaultConfig.AkamaiClientSecret, &cfg.AkamaiClientSecret)

provider/coredns/coredns.go

Lines changed: 76 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"time"
2929

3030
log "github.com/sirupsen/logrus"
31+
"go.etcd.io/etcd/api/v3/mvccpb"
3132
etcdcv3 "go.etcd.io/etcd/client/v3"
3233

3334
"sigs.k8s.io/external-dns/pkg/tlsutils"
@@ -84,10 +85,15 @@ type Service struct {
8485

8586
// Etcd key where we found this service and ignored from json un-/marshaling
8687
Key string `json:"-"`
88+
89+
// OwnedBy is used to prevent service to be added by different external-dns (only used by external-dns)
90+
OwnedBy string `json:"ownedby,omitempty"`
8791
}
8892

8993
type etcdClient struct {
90-
client *etcdcv3.Client
94+
client *etcdcv3.Client
95+
ownerID string
96+
strictlyOwned bool
9197
}
9298

9399
var _ coreDNSClient = etcdClient{}
@@ -106,11 +112,21 @@ func (c etcdClient) GetServices(ctx context.Context, prefix string) ([]*Service,
106112
var svcs []*Service
107113
bx := make(map[Service]bool)
108114
for _, n := range r.Kvs {
109-
svc := new(Service)
110-
if err := json.Unmarshal(n.Value, svc); err != nil {
111-
return nil, fmt.Errorf("%s: %w", n.Key, err)
115+
svc, err := c.unmarshalService(n)
116+
if err != nil {
117+
return nil, err
118+
}
119+
if c.strictlyOwned && svc.OwnedBy != c.ownerID {
120+
continue
121+
}
122+
b := Service{
123+
Host: svc.Host,
124+
Port: svc.Port,
125+
Priority: svc.Priority,
126+
Weight: svc.Weight,
127+
Text: svc.Text,
128+
Key: string(n.Key),
112129
}
113-
b := Service{Host: svc.Host, Port: svc.Port, Priority: svc.Priority, Weight: svc.Weight, Text: svc.Text, Key: string(n.Key)}
114130
if _, ok := bx[b]; ok {
115131
// skip the service if already added to service list.
116132
// the same service might be found in multiple etcd nodes.
@@ -132,6 +148,25 @@ func (c etcdClient) SaveService(ctx context.Context, service *Service) error {
132148
ctx, cancel := context.WithTimeout(ctx, etcdTimeout)
133149
defer cancel()
134150

151+
// check only for empty OwnedBy
152+
if c.strictlyOwned && service.OwnedBy != c.ownerID {
153+
r, err := c.client.Get(ctx, service.Key)
154+
if err != nil {
155+
return fmt.Errorf("etcd get %q: %w", service.Key, err)
156+
}
157+
// Key missing -> treat as owned (safe to create)
158+
if r != nil && len(r.Kvs) != 0 {
159+
svc, err := c.unmarshalService(r.Kvs[0])
160+
if err != nil {
161+
return fmt.Errorf("failed to unmarshal value for key %q: %w", service.Key, err)
162+
}
163+
if svc.OwnedBy != c.ownerID {
164+
return fmt.Errorf("key %q is not owned by this provider", service.Key)
165+
}
166+
}
167+
service.OwnedBy = c.ownerID
168+
}
169+
135170
value, err := json.Marshal(&service)
136171
if err != nil {
137172
return err
@@ -148,8 +183,38 @@ func (c etcdClient) DeleteService(ctx context.Context, key string) error {
148183
ctx, cancel := context.WithTimeout(ctx, etcdTimeout)
149184
defer cancel()
150185

151-
_, err := c.client.Delete(ctx, key, etcdcv3.WithPrefix())
152-
return err
186+
if c.strictlyOwned {
187+
rs, err := c.client.Get(ctx, key, etcdcv3.WithPrefix())
188+
if err != nil {
189+
return err
190+
}
191+
for _, r := range rs.Kvs {
192+
svc, err := c.unmarshalService(r)
193+
if err != nil {
194+
return err
195+
}
196+
if svc.OwnedBy != c.ownerID {
197+
continue
198+
}
199+
200+
_, err = c.client.Delete(ctx, string(r.Key))
201+
if err != nil {
202+
return err
203+
}
204+
}
205+
return err
206+
} else {
207+
_, err := c.client.Delete(ctx, key, etcdcv3.WithPrefix())
208+
return err
209+
}
210+
}
211+
212+
func (c etcdClient) unmarshalService(n *mvccpb.KeyValue) (*Service, error) {
213+
svc := new(Service)
214+
if err := json.Unmarshal(n.Value, svc); err != nil {
215+
return nil, fmt.Errorf("failed to unmarshal %q: %w", n.Key, err)
216+
}
217+
return svc, nil
153218
}
154219

155220
// builds etcd client config depending on connection scheme and TLS parameters
@@ -183,7 +248,7 @@ func getETCDConfig() (*etcdcv3.Config, error) {
183248
}
184249

185250
// the newETCDClient is an etcd client constructor
186-
func newETCDClient() (coreDNSClient, error) {
251+
func newETCDClient(ownerID string, strictlyOwned bool) (coreDNSClient, error) {
187252
cfg, err := getETCDConfig()
188253
if err != nil {
189254
return nil, err
@@ -192,12 +257,12 @@ func newETCDClient() (coreDNSClient, error) {
192257
if err != nil {
193258
return nil, err
194259
}
195-
return etcdClient{c}, nil
260+
return etcdClient{c, ownerID, strictlyOwned}, nil
196261
}
197262

198263
// NewCoreDNSProvider is a CoreDNS provider constructor
199-
func NewCoreDNSProvider(domainFilter *endpoint.DomainFilter, prefix string, dryRun bool) (provider.Provider, error) {
200-
client, err := newETCDClient()
264+
func NewCoreDNSProvider(domainFilter *endpoint.DomainFilter, prefix, ownerID string, strictlyOwned, dryRun bool) (provider.Provider, error) {
265+
client, err := newETCDClient(ownerID, strictlyOwned)
201266
if err != nil {
202267
return nil, err
203268
}

0 commit comments

Comments
 (0)