mirror of
https://github.com/coredns/coredns.git
synced 2025-11-02 18:23:25 -05:00
plugin/k8s_external: Resolve headless services (#5505)
*add option for resolving headless Services without external IPs in k8s_external Signed-off-by: Tomas Kohout <tomas.kohout1995@gmail.com>
This commit is contained in:
@@ -2,12 +2,12 @@
|
||||
|
||||
## Name
|
||||
|
||||
*k8s_external* - resolves load balancer and external IPs from outside Kubernetes clusters.
|
||||
*k8s_external* - resolves load balancer, external IPs from outside Kubernetes clusters and if enabled headless services.
|
||||
|
||||
## Description
|
||||
|
||||
This plugin allows an additional zone to resolve the external IP address(es) of a Kubernetes
|
||||
service. This plugin is only useful if the *kubernetes* plugin is also loaded.
|
||||
service and headless services. This plugin is only useful if the *kubernetes* plugin is also loaded.
|
||||
|
||||
The plugin uses an external zone to resolve in-cluster IP addresses. It only handles queries for A,
|
||||
AAAA, SRV, and PTR records; all others result in NODATA responses. To make it a proper DNS zone, it handles
|
||||
@@ -57,6 +57,16 @@ k8s_external [ZONE...] {
|
||||
* **APEX** is the name (DNS label) to use for the apex records; it defaults to `dns`.
|
||||
* `ttl` allows you to set a custom **TTL** for responses. The default is 5 (seconds).
|
||||
|
||||
If you want to enable headless service resolution, you can do so by adding `headless` option.
|
||||
|
||||
~~~
|
||||
k8s_external [ZONE...] {
|
||||
headless
|
||||
}
|
||||
~~~
|
||||
|
||||
* if there is a headless service with external IPs set, external IPs will be resolved
|
||||
|
||||
## Examples
|
||||
|
||||
Enable names under `example.org` to be resolved to in-cluster DNS addresses.
|
||||
|
||||
@@ -18,7 +18,7 @@ func (e *External) serveApex(state request.Request) (int, error) {
|
||||
case dns.TypeNS:
|
||||
m.Answer = []dns.RR{e.ns(state)}
|
||||
|
||||
addr := e.externalAddrFunc(state)
|
||||
addr := e.externalAddrFunc(state, e.headless)
|
||||
for _, rr := range addr {
|
||||
rr.Header().Ttl = e.ttl
|
||||
rr.Header().Name = dnsutil.Join("ns1", e.apex, state.QName())
|
||||
@@ -58,7 +58,7 @@ func (e *External) serveSubApex(state request.Request) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
addr := e.externalAddrFunc(state)
|
||||
addr := e.externalAddrFunc(state, e.headless)
|
||||
for _, rr := range addr {
|
||||
rr.Header().Ttl = e.ttl
|
||||
rr.Header().Name = state.QName()
|
||||
|
||||
@@ -17,6 +17,7 @@ func TestApex(t *testing.T) {
|
||||
k.APIConn = &external{}
|
||||
|
||||
e := New()
|
||||
e.headless = true
|
||||
e.Zones = []string{"example.com."}
|
||||
e.Next = test.NextHandler(dns.RcodeSuccess, nil)
|
||||
e.externalFunc = k.External
|
||||
|
||||
@@ -26,11 +26,11 @@ import (
|
||||
type Externaler interface {
|
||||
// External returns a slice of msg.Services that are looked up in the backend and match
|
||||
// the request.
|
||||
External(request.Request) ([]msg.Service, int)
|
||||
External(request.Request, bool) ([]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
|
||||
ExternalAddress(state request.Request, headless bool) []dns.RR
|
||||
// ExternalServices returns all services in the given zone as a slice of msg.Service and if enabled, headless services as a map of services.
|
||||
ExternalServices(zone string, headless bool) ([]msg.Service, map[string][]msg.Service)
|
||||
// ExternalSerial gets the current serial.
|
||||
ExternalSerial(string) uint32
|
||||
}
|
||||
@@ -40,16 +40,17 @@ type External struct {
|
||||
Next plugin.Handler
|
||||
Zones []string
|
||||
|
||||
hostmaster string
|
||||
apex string
|
||||
ttl uint32
|
||||
hostmaster string
|
||||
apex string
|
||||
ttl uint32
|
||||
headless bool
|
||||
|
||||
upstream *upstream.Upstream
|
||||
|
||||
externalFunc func(request.Request) ([]msg.Service, int)
|
||||
externalAddrFunc func(request.Request) []dns.RR
|
||||
externalFunc func(request.Request, bool) ([]msg.Service, int)
|
||||
externalAddrFunc func(request.Request, bool) []dns.RR
|
||||
externalSerialFunc func(string) uint32
|
||||
externalServicesFunc func(string) []msg.Service
|
||||
externalServicesFunc func(string, bool) ([]msg.Service, map[string][]msg.Service)
|
||||
}
|
||||
|
||||
// New returns a new and initialized *External.
|
||||
@@ -85,7 +86,7 @@ func (e *External) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Ms
|
||||
}
|
||||
}
|
||||
|
||||
svc, rcode := e.externalFunc(state)
|
||||
svc, rcode := e.externalFunc(state, e.headless)
|
||||
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(state.Req)
|
||||
|
||||
@@ -21,6 +21,7 @@ func TestExternal(t *testing.T) {
|
||||
|
||||
e := New()
|
||||
e.Zones = []string{"example.com.", "in-addr.arpa."}
|
||||
e.headless = true
|
||||
e.Next = test.NextHandler(dns.RcodeSuccess, nil)
|
||||
e.externalFunc = k.External
|
||||
e.externalAddrFunc = externalAddress // internal test function
|
||||
@@ -216,18 +217,86 @@ var tests = []test.Case{
|
||||
test.SRV("svc12.testns.example.com. 5 IN SRV 0 100 80 dummy.hostname."),
|
||||
},
|
||||
},
|
||||
// headless service
|
||||
{
|
||||
Qname: "svc-headless.testns.example.com.", Qtype: dns.TypeA, Rcode: dns.RcodeSuccess,
|
||||
Answer: []dns.RR{
|
||||
test.A("svc-headless.testns.example.com. 5 IN A 1.2.3.4"),
|
||||
test.A("svc-headless.testns.example.com. 5 IN A 1.2.3.5"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Qname: "svc-headless.testns.example.com.", Qtype: dns.TypeSRV, Rcode: dns.RcodeSuccess,
|
||||
Answer: []dns.RR{
|
||||
test.SRV("svc-headless.testns.example.com. 5 IN SRV 0 50 80 endpoint-svc-0.svc-headless.testns.example.com."),
|
||||
test.SRV("svc-headless.testns.example.com. 5 IN SRV 0 50 80 endpoint-svc-1.svc-headless.testns.example.com."),
|
||||
},
|
||||
Extra: []dns.RR{
|
||||
test.A("endpoint-svc-0.svc-headless.testns.example.com. 5 IN A 1.2.3.4"),
|
||||
test.A("endpoint-svc-1.svc-headless.testns.example.com. 5 IN A 1.2.3.5"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Qname: "_http._tcp.svc-headless.testns.example.com.", Qtype: dns.TypeSRV, Rcode: dns.RcodeSuccess,
|
||||
Answer: []dns.RR{
|
||||
test.SRV("_http._tcp.svc-headless.testns.example.com. 5 IN SRV 0 50 80 endpoint-svc-0.svc-headless.testns.example.com."),
|
||||
test.SRV("_http._tcp.svc-headless.testns.example.com. 5 IN SRV 0 50 80 endpoint-svc-1.svc-headless.testns.example.com."),
|
||||
},
|
||||
Extra: []dns.RR{
|
||||
test.A("endpoint-svc-0.svc-headless.testns.example.com. 5 IN A 1.2.3.4"),
|
||||
test.A("endpoint-svc-1.svc-headless.testns.example.com. 5 IN A 1.2.3.5"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Qname: "endpoint-svc-0.svc-headless.testns.example.com.", Qtype: dns.TypeSRV, Rcode: dns.RcodeSuccess,
|
||||
Answer: []dns.RR{
|
||||
test.SRV("endpoint-svc-0.svc-headless.testns.example.com. 5 IN SRV 0 100 80 endpoint-svc-0.svc-headless.testns.example.com."),
|
||||
},
|
||||
Extra: []dns.RR{
|
||||
test.A("endpoint-svc-0.svc-headless.testns.example.com. 5 IN A 1.2.3.4"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Qname: "endpoint-svc-1.svc-headless.testns.example.com.", Qtype: dns.TypeSRV, Rcode: dns.RcodeSuccess,
|
||||
Answer: []dns.RR{
|
||||
test.SRV("endpoint-svc-1.svc-headless.testns.example.com. 5 IN SRV 0 100 80 endpoint-svc-1.svc-headless.testns.example.com."),
|
||||
},
|
||||
Extra: []dns.RR{
|
||||
test.A("endpoint-svc-1.svc-headless.testns.example.com. 5 IN A 1.2.3.5"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Qname: "endpoint-svc-0.svc-headless.testns.example.com.", Qtype: dns.TypeA, Rcode: dns.RcodeSuccess,
|
||||
Answer: []dns.RR{
|
||||
test.A("endpoint-svc-0.svc-headless.testns.example.com. 5 IN A 1.2.3.4"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Qname: "endpoint-svc-1.svc-headless.testns.example.com.", Qtype: dns.TypeA, Rcode: dns.RcodeSuccess,
|
||||
Answer: []dns.RR{
|
||||
test.A("endpoint-svc-1.svc-headless.testns.example.com. 5 IN A 1.2.3.5"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type external struct{}
|
||||
|
||||
func (external) HasSynced() bool { return true }
|
||||
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(bool) int64 { return 0 }
|
||||
func (external) EpIndex(s string) []*object.Endpoints { return nil }
|
||||
func (external) EndpointsList() []*object.Endpoints { return nil }
|
||||
func (external) HasSynced() bool { return true }
|
||||
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(bool) int64 { return 0 }
|
||||
func (external) EpIndex(s string) []*object.Endpoints {
|
||||
return epIndexExternal[s]
|
||||
}
|
||||
func (external) EndpointsList() []*object.Endpoints {
|
||||
var eps []*object.Endpoints
|
||||
for _, ep := range epIndexExternal {
|
||||
eps = append(eps, ep...)
|
||||
}
|
||||
return eps
|
||||
}
|
||||
func (external) GetNodeByName(ctx context.Context, name string) (*api.Node, error) { return nil, nil }
|
||||
func (external) SvcIndex(s string) []*object.Service { return svcIndexExternal[s] }
|
||||
func (external) PodIndex(string) []*object.Pod { return nil }
|
||||
@@ -252,6 +321,41 @@ func (external) GetNamespaceByName(name string) (*object.Namespace, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
var epIndexExternal = map[string][]*object.Endpoints{
|
||||
"svc-headless.testns": {
|
||||
{
|
||||
Name: "svc-headless",
|
||||
Namespace: "testns",
|
||||
Index: "svc-headless.testns",
|
||||
Subsets: []object.EndpointSubset{
|
||||
{
|
||||
Ports: []object.EndpointPort{
|
||||
{
|
||||
Port: 80,
|
||||
Name: "http",
|
||||
Protocol: "TCP",
|
||||
},
|
||||
},
|
||||
Addresses: []object.EndpointAddress{
|
||||
{
|
||||
IP: "1.2.3.4",
|
||||
Hostname: "endpoint-svc-0",
|
||||
NodeName: "test-node",
|
||||
TargetRefName: "endpoint-svc-0",
|
||||
},
|
||||
{
|
||||
IP: "1.2.3.5",
|
||||
Hostname: "endpoint-svc-1",
|
||||
NodeName: "test-node",
|
||||
TargetRefName: "endpoint-svc-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var svcIndexExternal = map[string][]*object.Service{
|
||||
"svc1.testns": {
|
||||
{
|
||||
@@ -279,6 +383,7 @@ var svcIndexExternal = map[string][]*object.Service{
|
||||
Namespace: "testns",
|
||||
Type: api.ServiceTypeLoadBalancer,
|
||||
ExternalIPs: []string{"2.3.4.5"},
|
||||
ClusterIPs: []string{"10.0.0.3"},
|
||||
Ports: []api.ServicePort{{Name: "http", Protocol: "tcp", Port: 80}},
|
||||
},
|
||||
},
|
||||
@@ -287,10 +392,20 @@ var svcIndexExternal = map[string][]*object.Service{
|
||||
Name: "svc12",
|
||||
Namespace: "testns",
|
||||
Type: api.ServiceTypeLoadBalancer,
|
||||
ClusterIPs: []string{"10.0.0.3"},
|
||||
ExternalIPs: []string{"dummy.hostname"},
|
||||
Ports: []api.ServicePort{{Name: "http", Protocol: "tcp", Port: 80}},
|
||||
},
|
||||
},
|
||||
"svc-headless.testns": {
|
||||
{
|
||||
Name: "svc-headless",
|
||||
Namespace: "testns",
|
||||
Type: api.ServiceTypeClusterIP,
|
||||
ClusterIPs: []string{"None"},
|
||||
Ports: []api.ServicePort{{Name: "http", Protocol: "tcp", Port: 80}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func (external) ServiceList() []*object.Service {
|
||||
@@ -301,7 +416,7 @@ func (external) ServiceList() []*object.Service {
|
||||
return svcs
|
||||
}
|
||||
|
||||
func externalAddress(state request.Request) []dns.RR {
|
||||
func externalAddress(state request.Request, headless bool) []dns.RR {
|
||||
a := test.A("example.org. IN A 127.0.0.1")
|
||||
return []dns.RR{a}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,8 @@ func parse(c *caddy.Controller) (*External, error) {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
e.apex = args[0]
|
||||
case "headless":
|
||||
e.headless = true
|
||||
default:
|
||||
return nil, c.Errf("unknown property '%s'", c.Val())
|
||||
}
|
||||
|
||||
@@ -8,16 +8,20 @@ import (
|
||||
|
||||
func TestSetup(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
expectedZone string
|
||||
expectedApex string
|
||||
input string
|
||||
shouldErr bool
|
||||
expectedZone string
|
||||
expectedApex string
|
||||
expectedHeadless bool
|
||||
}{
|
||||
{`k8s_external`, false, "", "dns"},
|
||||
{`k8s_external example.org`, false, "example.org.", "dns"},
|
||||
{`k8s_external`, false, "", "dns", false},
|
||||
{`k8s_external example.org`, false, "example.org.", "dns", false},
|
||||
{`k8s_external example.org {
|
||||
apex testdns
|
||||
}`, false, "example.org.", "testdns"},
|
||||
}`, false, "example.org.", "testdns", false},
|
||||
{`k8s_external example.org {
|
||||
headless
|
||||
}`, false, "example.org.", "dns", true},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
@@ -44,5 +48,10 @@ func TestSetup(t *testing.T) {
|
||||
t.Errorf("Test %d, expected apex %q for input %s, got: %q", i, test.expectedApex, test.input, e.apex)
|
||||
}
|
||||
}
|
||||
if !test.shouldErr {
|
||||
if test.expectedHeadless != e.headless {
|
||||
t.Errorf("Test %d, expected headless %q for input %s, got: %v", i, test.expectedApex, test.input, e.headless)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,11 @@ package external
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/coredns/coredns/plugin"
|
||||
"github.com/coredns/coredns/plugin/etcd/msg"
|
||||
"github.com/coredns/coredns/plugin/kubernetes"
|
||||
"github.com/coredns/coredns/plugin/transfer"
|
||||
"github.com/coredns/coredns/request"
|
||||
|
||||
@@ -40,7 +42,7 @@ func (e *External) Transfer(zone string, serial uint32) (<-chan []dns.RR, error)
|
||||
ch <- []dns.RR{&dns.NS{Hdr: nsHdr, Ns: nsName}}
|
||||
|
||||
// Add Nameserver A/AAAA records
|
||||
nsRecords := e.externalAddrFunc(state)
|
||||
nsRecords := e.externalAddrFunc(state, e.headless)
|
||||
for i := range nsRecords {
|
||||
// externalAddrFunc returns incomplete header names, correct here
|
||||
nsRecords[i].Header().Name = nsName
|
||||
@@ -48,10 +50,12 @@ func (e *External) Transfer(zone string, serial uint32) (<-chan []dns.RR, error)
|
||||
ch <- []dns.RR{nsRecords[i]}
|
||||
}
|
||||
|
||||
svcs := e.externalServicesFunc(zone)
|
||||
svcs, headlessSvcs := e.externalServicesFunc(zone, e.headless)
|
||||
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}}}}
|
||||
@@ -81,6 +85,55 @@ func (e *External) Transfer(zone string, serial uint32) (<-chan []dns.RR, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
for key, svcs := range headlessSvcs {
|
||||
// we have to strip the leading key because it's either port.protocol or endpoint
|
||||
name := msg.Domain(key[:strings.LastIndex(key, "/")])
|
||||
switchKey := key[strings.LastIndex(key, "/")+1:]
|
||||
switch switchKey {
|
||||
case kubernetes.Endpoint:
|
||||
// headless.namespace.example.com records
|
||||
s := request.Request{Req: &dns.Msg{Question: []dns.Question{{Name: name}}}}
|
||||
as, _ := e.a(ctx, svcs, s)
|
||||
if len(as) > 0 {
|
||||
ch <- as
|
||||
}
|
||||
aaaas, _ := e.aaaa(ctx, svcs, s)
|
||||
if len(aaaas) > 0 {
|
||||
ch <- aaaas
|
||||
}
|
||||
// Add bare SRV record, ensuring uniqueness
|
||||
recs, _ := e.srv(ctx, svcs, s)
|
||||
ch <- recs
|
||||
for _, srv := range recs {
|
||||
ch <- []dns.RR{srv}
|
||||
}
|
||||
|
||||
for i := range svcs {
|
||||
// endpoint.headless.namespace.example.com record
|
||||
s := request.Request{Req: &dns.Msg{Question: []dns.Question{{Name: msg.Domain(svcs[i].Key)}}}}
|
||||
|
||||
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)
|
||||
ch <- recs
|
||||
for _, srv := range recs {
|
||||
ch <- []dns.RR{srv}
|
||||
}
|
||||
}
|
||||
|
||||
case kubernetes.PortProtocol:
|
||||
s := request.Request{Req: &dns.Msg{Question: []dns.Question{{Name: name}}}}
|
||||
recs, _ := e.srv(ctx, svcs, s)
|
||||
ch <- recs
|
||||
}
|
||||
}
|
||||
ch <- []dns.RR{soa}
|
||||
close(ch)
|
||||
}()
|
||||
|
||||
@@ -27,6 +27,7 @@ func TestTransferAXFR(t *testing.T) {
|
||||
k.APIConn = &external{}
|
||||
|
||||
e := New()
|
||||
e.headless = true
|
||||
e.Zones = []string{"example.com."}
|
||||
e.externalFunc = k.External
|
||||
e.externalAddrFunc = externalAddress // internal test function
|
||||
@@ -64,6 +65,7 @@ func TestTransferAXFR(t *testing.T) {
|
||||
if ans.Header().Rrtype == dns.TypePTR {
|
||||
continue
|
||||
}
|
||||
|
||||
expect = append(expect, ans)
|
||||
}
|
||||
}
|
||||
@@ -92,6 +94,7 @@ func TestTransferIXFR(t *testing.T) {
|
||||
|
||||
e := New()
|
||||
e.Zones = []string{"example.com."}
|
||||
e.headless = true
|
||||
e.externalFunc = k.External
|
||||
e.externalAddrFunc = externalAddress // internal test function
|
||||
e.externalSerialFunc = externalSerial // internal test function
|
||||
|
||||
Reference in New Issue
Block a user