plugin/k8s_external: implement zone transfers (#4977)

Implement transfer for k8s_external. Notifies not supported.

Signed-off-by: Chris O'Haver <cohaver@infoblox.com>
This commit is contained in:
Chris O'Haver
2022-03-07 12:16:24 -05:00
committed by GitHub
parent 267ce8a820
commit 7263808fe1
17 changed files with 493 additions and 30 deletions

View File

@@ -83,6 +83,18 @@ spec:
type: ClusterIP
~~~
The *k8s_external* plugin can be used in conjunction with the *transfer* plugin to enable
zone transfers. Notifies are not supported.
~~~
. {
transfer example.org {
to *
}
kubernetes cluster.local
k8s_external example.org
}
~~~
# See Also

View File

@@ -93,7 +93,7 @@ func (e *External) soa(state request.Request) *dns.SOA {
soa := &dns.SOA{Hdr: header,
Mbox: dnsutil.Join(e.hostmaster, e.apex, state.Zone),
Ns: dnsutil.Join("ns1", e.apex, state.Zone),
Serial: 12345, // Also dynamic?
Serial: e.externalSerialFunc(state.Zone),
Refresh: 7200,
Retry: 1800,
Expire: 86400,

View File

@@ -20,7 +20,8 @@ func TestApex(t *testing.T) {
e.Zones = []string{"example.com."}
e.Next = test.NextHandler(dns.RcodeSuccess, nil)
e.externalFunc = k.External
e.externalAddrFunc = externalAddress // internal test function
e.externalAddrFunc = externalAddress // internal test function
e.externalSerialFunc = externalSerial // internal test function
ctx := context.TODO()
for i, tc := range testsApex {
@@ -43,6 +44,16 @@ func TestApex(t *testing.T) {
if err := test.SortAndCheck(resp, tc); err != nil {
t.Error(err)
}
for i, rr := range tc.Ns {
expectsoa := rr.(*dns.SOA)
gotsoa, ok := resp.Ns[i].(*dns.SOA)
if !ok {
t.Fatalf("Unexpected record type in Authority section")
}
if expectsoa.Serial != gotsoa.Serial {
t.Fatalf("Expected soa serial %d, got %d", expectsoa.Serial, gotsoa.Serial)
}
}
}
}
@@ -50,7 +61,7 @@ var testsApex = []test.Case{
{
Qname: "example.com.", Qtype: dns.TypeSOA, Rcode: dns.RcodeSuccess,
Answer: []dns.RR{
test.SOA("example.com. 5 IN SOA ns1.dns.example.com. hostmaster.example.com. 1499347823 7200 1800 86400 5"),
test.SOA("example.com. 5 IN SOA ns1.dns.example.com. hostmaster.dns.example.com. 1499347823 7200 1800 86400 5"),
},
},
{
@@ -65,37 +76,37 @@ var testsApex = []test.Case{
{
Qname: "example.com.", Qtype: dns.TypeSRV, Rcode: dns.RcodeSuccess,
Ns: []dns.RR{
test.SOA("example.com. 5 IN SOA ns1.dns.example.com. hostmaster.example.com. 1499347823 7200 1800 86400 5"),
test.SOA("example.com. 5 IN SOA ns1.dns.example.com. hostmaster.dns.example.com. 1499347823 7200 1800 86400 5"),
},
},
{
Qname: "dns.example.com.", Qtype: dns.TypeSRV, Rcode: dns.RcodeSuccess,
Ns: []dns.RR{
test.SOA("example.com. 5 IN SOA ns1.dns.example.com. hostmaster.example.com. 1499347823 7200 1800 86400 5"),
test.SOA("example.com. 5 IN SOA ns1.dns.example.com. hostmaster.dns.example.com. 1499347823 7200 1800 86400 5"),
},
},
{
Qname: "dns.example.com.", Qtype: dns.TypeNS, Rcode: dns.RcodeSuccess,
Ns: []dns.RR{
test.SOA("example.com. 5 IN SOA ns1.dns.example.com. hostmaster.example.com. 1499347823 7200 1800 86400 5"),
test.SOA("example.com. 5 IN SOA ns1.dns.example.com. hostmaster.dns.example.com. 1499347823 7200 1800 86400 5"),
},
},
{
Qname: "ns1.dns.example.com.", Qtype: dns.TypeSRV, Rcode: dns.RcodeSuccess,
Ns: []dns.RR{
test.SOA("example.com. 5 IN SOA ns1.dns.example.com. hostmaster.example.com. 1499347823 7200 1800 86400 5"),
test.SOA("example.com. 5 IN SOA ns1.dns.example.com. hostmaster.dns.example.com. 1499347823 7200 1800 86400 5"),
},
},
{
Qname: "ns1.dns.example.com.", Qtype: dns.TypeNS, Rcode: dns.RcodeSuccess,
Ns: []dns.RR{
test.SOA("example.com. 5 IN SOA ns1.dns.example.com. hostmaster.example.com. 1499347823 7200 1800 86400 5"),
test.SOA("example.com. 5 IN SOA ns1.dns.example.com. hostmaster.dns.example.com. 1499347823 7200 1800 86400 5"),
},
},
{
Qname: "ns1.dns.example.com.", Qtype: dns.TypeAAAA, Rcode: dns.RcodeSuccess,
Ns: []dns.RR{
test.SOA("example.com. 5 IN SOA ns1.dns.example.com. hostmaster.example.com. 1499347823 7200 1800 86400 5"),
test.SOA("example.com. 5 IN SOA ns1.dns.example.com. hostmaster.dns.example.com. 1499347823 7200 1800 86400 5"),
},
},
{

View File

@@ -29,9 +29,13 @@ type Externaler interface {
External(request.Request) ([]msg.Service, int)
// ExternalAddress should return a string slice of addresses for the nameserving endpoint.
ExternalAddress(state request.Request) []dns.RR
// ExternalServices returns all services in the given zone as a slice of msg.Service.
ExternalServices(zone string) []msg.Service
// ExternalSerial gets the current serial.
ExternalSerial(string) uint32
}
// External resolves Ingress and Loadbalance IPs from kubernetes clusters.
// External serves records for External IPs and Loadbalance IPs of Services in Kubernetes clusters.
type External struct {
Next plugin.Handler
Zones []string
@@ -42,8 +46,10 @@ type External struct {
upstream *upstream.Upstream
externalFunc func(request.Request) ([]msg.Service, int)
externalAddrFunc func(request.Request) []dns.RR
externalFunc func(request.Request) ([]msg.Service, int)
externalAddrFunc func(request.Request) []dns.RR
externalSerialFunc func(string) uint32
externalServicesFunc func(string) []msg.Service
}
// New returns a new and initialized *External.

View File

@@ -23,7 +23,8 @@ func TestExternal(t *testing.T) {
e.Zones = []string{"example.com."}
e.Next = test.NextHandler(dns.RcodeSuccess, nil)
e.externalFunc = k.External
e.externalAddrFunc = externalAddress // internal test function
e.externalAddrFunc = externalAddress // internal test function
e.externalSerialFunc = externalSerial // internal test function
ctx := context.TODO()
for i, tc := range tests {
@@ -147,12 +148,38 @@ var tests = []test.Case{
test.SOA("example.com. 5 IN SOA ns1.dns.example.com. hostmaster.example.com. 1499347823 7200 1800 86400 5"),
},
},
// svc11
{
Qname: "svc11.testns.example.com.", Qtype: dns.TypeA, Rcode: dns.RcodeSuccess,
Answer: []dns.RR{
test.A("svc11.testns.example.com. 5 IN A 1.2.3.4"),
},
},
{
Qname: "_http._tcp.svc11.testns.example.com.", Qtype: dns.TypeSRV, Rcode: dns.RcodeSuccess,
Answer: []dns.RR{
test.SRV("_http._tcp.svc11.testns.example.com. 5 IN SRV 0 100 80 svc11.testns.example.com."),
},
Extra: []dns.RR{
test.A("svc11.testns.example.com. 5 IN A 1.2.3.4"),
},
},
{
Qname: "svc11.testns.example.com.", Qtype: dns.TypeSRV, Rcode: dns.RcodeSuccess,
Answer: []dns.RR{
test.SRV("svc11.testns.example.com. 5 IN SRV 0 100 80 svc11.testns.example.com."),
},
Extra: []dns.RR{
test.A("svc11.testns.example.com. 5 IN A 1.2.3.4"),
},
},
// svc12
{
Qname: "svc12.testns.example.com.", Qtype: dns.TypeA, Rcode: dns.RcodeSuccess,
Answer: []dns.RR{
test.CNAME("svc12.testns.example.com. 5 IN CNAME dummy.hostname"),
},
},
{
Qname: "_http._tcp.svc12.testns.example.com.", Qtype: dns.TypeSRV, Rcode: dns.RcodeSuccess,
Answer: []dns.RR{
@@ -160,9 +187,9 @@ var tests = []test.Case{
},
},
{
Qname: "svc12.testns.example.com.", Qtype: dns.TypeA, Rcode: dns.RcodeSuccess,
Qname: "svc12.testns.example.com.", Qtype: dns.TypeSRV, Rcode: dns.RcodeSuccess,
Answer: []dns.RR{
test.CNAME("svc12.testns.example.com. 5 IN CNAME dummy.hostname"),
test.SRV("svc12.testns.example.com. 5 IN SRV 0 100 80 dummy.hostname."),
},
},
}
@@ -173,8 +200,8 @@ func (external) HasSynced() bool
func (external) Run() {}
func (external) Stop() error { return nil }
func (external) EpIndexReverse(string) []*object.Endpoints { return nil }
func (external) SvcIndexReverse(string) []*object.Service { return nil }
func (external) Modified() int64 { return 0 }
func (external) SvcIndexReverse(string) []*object.Service { return nil }
func (external) Modified(bool) int64 { return 0 }
func (external) EpIndex(s string) []*object.Endpoints { return nil }
func (external) EndpointsList() []*object.Endpoints { return nil }
func (external) GetNodeByName(ctx context.Context, name string) (*api.Node, error) { return nil, nil }
@@ -240,3 +267,7 @@ func externalAddress(state request.Request) []dns.RR {
a := test.A("example.org. IN A 127.0.0.1")
return []dns.RR{a}
}
func externalSerial(string) uint32 {
return 1499347823
}

View File

@@ -26,6 +26,8 @@ func setup(c *caddy.Controller) error {
if x, ok := m.(Externaler); ok {
e.externalFunc = x.External
e.externalAddrFunc = x.ExternalAddress
e.externalServicesFunc = x.ExternalServices
e.externalSerialFunc = x.ExternalSerial
}
return nil
})

View File

@@ -0,0 +1,97 @@
package external
import (
"context"
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/etcd/msg"
"github.com/coredns/coredns/plugin/transfer"
"github.com/coredns/coredns/request"
"github.com/miekg/dns"
)
// Transfer implements transfer.Transferer
func (e *External) Transfer(zone string, serial uint32) (<-chan []dns.RR, error) {
z := plugin.Zones(e.Zones).Matches(zone)
if z != zone {
return nil, transfer.ErrNotAuthoritative
}
ctx := context.Background()
ch := make(chan []dns.RR, 2)
if zone == "." {
zone = ""
}
state := request.Request{Zone: zone}
// SOA
soa := e.soa(state)
ch <- []dns.RR{soa}
if serial != 0 && serial >= soa.Serial {
close(ch)
return ch, nil
}
go func() {
// Add NS
nsName := "ns1." + e.apex + "." + zone
nsHdr := dns.RR_Header{Name: zone, Rrtype: dns.TypeNS, Ttl: e.ttl, Class: dns.ClassINET}
ch <- []dns.RR{&dns.NS{Hdr: nsHdr, Ns: nsName}}
// Add Nameserver A/AAAA records
nsRecords := e.externalAddrFunc(state)
for i := range nsRecords {
// externalAddrFunc returns incomplete header names, correct here
nsRecords[i].Header().Name = nsName
nsRecords[i].Header().Ttl = e.ttl
ch <- []dns.RR{nsRecords[i]}
}
svcs := e.externalServicesFunc(zone)
srvSeen := make(map[string]struct{})
for i := range svcs {
name := msg.Domain(svcs[i].Key)
if svcs[i].TargetStrip == 0 {
// Add Service A/AAAA records
s := request.Request{Req: &dns.Msg{Question: []dns.Question{{Name: name}}}}
as := e.a(ctx, []msg.Service{svcs[i]}, s)
if len(as) > 0 {
ch <- as
}
aaaas := e.aaaa(ctx, []msg.Service{svcs[i]}, s)
if len(aaaas) > 0 {
ch <- aaaas
}
// Add bare SRV record, ensuring uniqueness
recs, _ := e.srv(ctx, []msg.Service{svcs[i]}, s)
for _, srv := range recs {
if !nameSeen(srvSeen, srv) {
ch <- []dns.RR{srv}
}
}
continue
}
// Add full SRV record, ensuring uniqueness
s := request.Request{Req: &dns.Msg{Question: []dns.Question{{Name: name}}}}
recs, _ := e.srv(ctx, []msg.Service{svcs[i]}, s)
for _, srv := range recs {
if !nameSeen(srvSeen, srv) {
ch <- []dns.RR{srv}
}
}
}
ch <- []dns.RR{soa}
close(ch)
}()
return ch, nil
}
func nameSeen(namesSeen map[string]struct{}, rr dns.RR) bool {
if _, duplicate := namesSeen[rr.Header().Name]; duplicate {
return true
}
namesSeen[rr.Header().Name] = struct{}{}
return false
}

View File

@@ -0,0 +1,142 @@
package external
import (
"strings"
"testing"
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/kubernetes"
"github.com/coredns/coredns/plugin/test"
"github.com/coredns/coredns/plugin/transfer"
"github.com/miekg/dns"
)
func TestImplementsTransferer(t *testing.T) {
var e plugin.Handler
e = &External{}
_, ok := e.(transfer.Transferer)
if !ok {
t.Error("Transferer not implemented")
}
}
func TestTransferAXFR(t *testing.T) {
k := kubernetes.New([]string{"cluster.local."})
k.Namespaces = map[string]struct{}{"testns": {}}
k.APIConn = &external{}
e := New()
e.Zones = []string{"example.com."}
e.externalFunc = k.External
e.externalAddrFunc = externalAddress // internal test function
e.externalSerialFunc = externalSerial // internal test function
e.externalServicesFunc = k.ExternalServices
ch, err := e.Transfer("example.com.", 0)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
var records []dns.RR
for rrs := range ch {
records = append(records, rrs...)
}
expect := []dns.RR{}
for _, tc := range append(tests, testsApex...) {
if tc.Rcode != dns.RcodeSuccess {
continue
}
for _, ans := range tc.Answer {
// Exclude wildcard test cases
if strings.Contains(ans.Header().Name, "*") {
continue
}
// Exclude TXT records
if ans.Header().Rrtype == dns.TypeTXT {
continue
}
expect = append(expect, ans)
}
}
diff := difference(expect, records)
if len(diff) != 0 {
t.Errorf("Got back %d records that do not exist in test cases, should be 0:", len(diff))
for _, rec := range diff {
t.Errorf("%+v", rec)
}
}
diff = difference(records, expect)
if len(diff) != 0 {
t.Errorf("Result is missing %d records, should be 0:", len(diff))
for _, rec := range diff {
t.Errorf("%+v", rec)
}
}
}
func TestTransferIXFR(t *testing.T) {
k := kubernetes.New([]string{"cluster.local."})
k.Namespaces = map[string]struct{}{"testns": {}}
k.APIConn = &external{}
e := New()
e.Zones = []string{"example.com."}
e.externalFunc = k.External
e.externalAddrFunc = externalAddress // internal test function
e.externalSerialFunc = externalSerial // internal test function
e.externalServicesFunc = k.ExternalServices
ch, err := e.Transfer("example.com.", externalSerial("example.com."))
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
var records []dns.RR
for rrs := range ch {
records = append(records, rrs...)
}
expect := []dns.RR{
test.SOA("example.com. 5 IN SOA ns1.dns.example.com. hostmaster.dns.example.com. 1499347823 7200 1800 86400 5"),
}
diff := difference(expect, records)
if len(diff) != 0 {
t.Errorf("Got back %d records that do not exist in test cases, should be 0:", len(diff))
for _, rec := range diff {
t.Errorf("%+v", rec)
}
}
diff = difference(records, expect)
if len(diff) != 0 {
t.Errorf("Result is missing %d records, should be 0:", len(diff))
for _, rec := range diff {
t.Errorf("%+v", rec)
}
}
}
// difference shows what we're missing when comparing two RR slices
func difference(testRRs []dns.RR, gotRRs []dns.RR) []dns.RR {
expectedRRs := map[string]struct{}{}
for _, rr := range testRRs {
expectedRRs[rr.String()] = struct{}{}
}
foundRRs := []dns.RR{}
for _, rr := range gotRRs {
if _, ok := expectedRRs[rr.String()]; !ok {
foundRRs = append(foundRRs, rr)
}
}
return foundRRs
}