Add new plugin: external - resolve k8s ingress and LB address with external names (#2379)

* Add new plugin: external

This plugin works in conjunction with the kubernetes plugin and exports
ingress and LB addresses as DNS records. It bypasses backend.go and
backend_lookup.go flow because it is not needed.

README, tests are implemented. The tests only exercise the unit tests,
this has not been tested in any ci.

Signed-off-by: Miek Gieben <miek@miek.nl>

* Rename to k8s_external

Signed-off-by: Miek Gieben <miek@miek.nl>

* go gen

Signed-off-by: Miek Gieben <miek@miek.nl>
This commit is contained in:
Miek Gieben 2018-12-14 09:41:51 +00:00 committed by GitHub
parent d9880681c3
commit c1c98924c3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 1156 additions and 1 deletions

View file

@ -34,6 +34,7 @@ var Directives = []string{
"hosts",
"route53",
"federation",
"k8s_external",
"kubernetes",
"file",
"auto",

View file

@ -20,6 +20,7 @@ import (
_ "github.com/coredns/coredns/plugin/forward"
_ "github.com/coredns/coredns/plugin/health"
_ "github.com/coredns/coredns/plugin/hosts"
_ "github.com/coredns/coredns/plugin/k8s_external"
_ "github.com/coredns/coredns/plugin/kubernetes"
_ "github.com/coredns/coredns/plugin/loadbalance"
_ "github.com/coredns/coredns/plugin/log"

View file

@ -43,6 +43,7 @@ template:template
hosts:hosts
route53:route53
federation:federation
k8s_external:k8s_external
kubernetes:kubernetes
file:file
auto:auto

View file

@ -0,0 +1,4 @@
reviewers:
- miekg
approvers:
- miekg

View file

@ -0,0 +1,78 @@
# k8s_external
## Name
*k8s_external* - resolve load balancer and external IPs from outside kubernetes clusters.
## 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.
The plugin uses an external zone to resolve in-cluster IP addresses. It only handles queries for A,
AAAA and SRV records, all others result in NODATA responses. To make it a proper DNS zone it handles
SOA and NS queries for the apex of the zone.
By default the apex of the zone will look like (assuming the zone used is `example.org`):
~~~ dns
example.org. 5 IN SOA ns1.dns.example.org. hostmaster.example.org. (
12345 ; serial
14400 ; refresh (4 hours)
3600 ; retry (1 hour)
604800 ; expire (1 week)
5 ; minimum (4 hours)
)
example.org 5 IN NS ns1.dns.example.org.
ns1.dns.example.org. 5 IN A ....
ns1.dns.example.org. 5 IN AAAA ....
~~~
Note we use the `dns` subdomain to place the records the DNS needs (see the `apex` directive). Also
note the SOA's serial number is static. The IP addresses of the nameserver records are those of the
CoreDNS service.
The *k8s_external* plugin handles the subdomain `dns` and the apex of the zone by itself, all other
queries are resolved to addresses in the cluster.
## Syntax
~~~
k8s_external [ZONE...]
~~~
* **ZONES** zones *k8s_external* should be authoritative for.
If you want to change the apex domain or use a different TTL for the return records you can use
this extended syntax.
~~~
k8s_external [ZONE...] {
apex APEX
ttl TTL
}
~~~
* **APEX** is the name (DNS label) to use the apex records, defaults to `dns`.
* `ttl` allows you to set a custom **TTL** for responses. The default is 5 (seconds).
# Examples
Enable names under `example.org` to be resolved to in cluster DNS addresses.
~~~
. {
kubernetes cluster.local
k8s_external example.org
}
~~~
# Also See
For some background see [resolve external IP address](https://github.com/kubernetes/dns/issues/242).
And [A records for services with Load Balancer IP](https://github.com/coredns/coredns/issues/1851).
# Bugs
PTR queries for the reverse zone is not supported.

110
plugin/k8s_external/apex.go Normal file
View file

@ -0,0 +1,110 @@
package external
import (
"github.com/coredns/coredns/plugin/pkg/dnsutil"
"github.com/coredns/coredns/request"
"github.com/miekg/dns"
)
// serveApex serves request that hit the zone' apex. A reply is written back to the client.
func (e *External) serveApex(state request.Request) (int, error) {
m := new(dns.Msg)
m.SetReply(state.Req)
switch state.QType() {
case dns.TypeSOA:
m.Answer = []dns.RR{e.soa(state)}
case dns.TypeNS:
m.Answer = []dns.RR{e.ns(state)}
addr := e.externalAddrFunc(state)
for _, rr := range addr {
rr.Header().Ttl = e.ttl
rr.Header().Name = state.QName()
m.Extra = append(m.Extra, rr)
}
default:
m.Ns = []dns.RR{e.soa(state)}
}
state.W.WriteMsg(m)
return 0, nil
}
// serveSubApex serves requests that hit the zones fake 'dns' subdomain where our nameservers live.
func (e *External) serveSubApex(state request.Request) (int, error) {
base, _ := dnsutil.TrimZone(state.Name(), state.Zone)
m := new(dns.Msg)
m.SetReply(state.Req)
// base is either dns. of ns1.dns (or another name), if it's longer return nxdomain
switch labels := dns.CountLabel(base); labels {
default:
m.SetRcode(m, dns.RcodeNameError)
m.Ns = []dns.RR{e.soa(state)}
state.W.WriteMsg(m)
return 0, nil
case 2:
nl, _ := dns.NextLabel(base, 0)
ns := base[:nl]
if ns != "ns1." {
// nxdomain
m.SetRcode(m, dns.RcodeNameError)
m.Ns = []dns.RR{e.soa(state)}
state.W.WriteMsg(m)
return 0, nil
}
addr := e.externalAddrFunc(state)
for _, rr := range addr {
rr.Header().Ttl = e.ttl
rr.Header().Name = state.QName()
switch state.QType() {
case dns.TypeA:
if rr.Header().Rrtype == dns.TypeA {
m.Answer = append(m.Answer, rr)
}
case dns.TypeAAAA:
if rr.Header().Rrtype == dns.TypeAAAA {
m.Answer = append(m.Answer, rr)
}
}
}
if len(m.Answer) == 0 {
m.Ns = []dns.RR{e.soa(state)}
}
state.W.WriteMsg(m)
return 0, nil
case 1:
// nodata for the dns empty non-terminal
m.Ns = []dns.RR{e.soa(state)}
state.W.WriteMsg(m)
return 0, nil
}
}
func (e *External) soa(state request.Request) *dns.SOA {
header := dns.RR_Header{Name: state.Zone, Rrtype: dns.TypeSOA, Ttl: e.ttl, Class: dns.ClassINET}
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?
Refresh: 7200,
Retry: 1800,
Expire: 86400,
Minttl: e.ttl,
}
return soa
}
func (e *External) ns(state request.Request) *dns.NS {
header := dns.RR_Header{Name: state.Zone, Rrtype: dns.TypeNS, Ttl: e.ttl, Class: dns.ClassINET}
ns := &dns.NS{Hdr: header, Ns: dnsutil.Join("ns1", e.apex, state.Zone)}
return ns
}

View file

@ -0,0 +1,105 @@
package external
import (
"context"
"testing"
"github.com/coredns/coredns/plugin/kubernetes"
"github.com/coredns/coredns/plugin/pkg/dnstest"
"github.com/coredns/coredns/plugin/test"
"github.com/miekg/dns"
)
func TestApex(t *testing.T) {
k := kubernetes.New([]string{"cluster.local."})
k.Namespaces = map[string]struct{}{"testns": struct{}{}}
k.APIConn = &external{}
e := New()
e.Zones = []string{"example.com."}
e.Next = test.NextHandler(dns.RcodeSuccess, nil)
e.externalFunc = k.External
e.externalAddrFunc = externalAddress // internal test function
ctx := context.TODO()
for i, tc := range testsApex {
r := tc.Msg()
w := dnstest.NewRecorder(&test.ResponseWriter{})
_, err := e.ServeDNS(ctx, w, r)
if err != tc.Error {
t.Errorf("Test %d expected no error, got %v", i, err)
return
}
if tc.Error != nil {
continue
}
resp := w.Msg
if resp == nil {
t.Fatalf("Test %d, got nil message and no error for %q", i, r.Question[0].Name)
}
test.SortAndCheck(t, resp, tc)
}
}
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"),
},
},
{
Qname: "example.com.", Qtype: dns.TypeNS, Rcode: dns.RcodeSuccess,
Answer: []dns.RR{
test.NS("example.com. 5 IN NS ns1.dns.example.com."),
},
Extra: []dns.RR{
test.A("example.com. 5 IN A 127.0.0.1"),
},
},
{
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"),
},
},
{
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"),
},
},
{
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"),
},
},
{
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"),
},
},
{
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"),
},
},
{
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"),
},
},
{
Qname: "ns1.dns.example.com.", Qtype: dns.TypeA, Rcode: dns.RcodeSuccess,
Answer: []dns.RR{
test.A("ns1.dns.example.com. 5 IN A 127.0.0.1"),
},
},
}

View file

@ -0,0 +1,112 @@
/*
Package external implements external names for kubernetes clusters.
This plugin only handles three qtypes (except the apex queries, because those are handled
differently). We support A, AAAA and SRV request, for all other types we return NODATA or
NXDOMAIN depending on the state of the cluster.
A plugin willing to provide these services must implement the Externaler interface, although it
likely only makes sense for the *kubernetes* plugin.
*/
package external
import (
"context"
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/etcd/msg"
"github.com/coredns/coredns/request"
"github.com/miekg/dns"
)
// Externaler defines the interface that a plugin should implement in order to be used by External.
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)
// ExternalAddress should return a string slice of addresses for the nameserving endpoint.
ExternalAddress(state request.Request) []dns.RR
}
// External resolves Ingress and Loadbalance IPs from kubernetes clusters.
type External struct {
Next plugin.Handler
Zones []string
hostmaster string
apex string
ttl uint32
externalFunc func(request.Request) ([]msg.Service, int)
externalAddrFunc func(request.Request) []dns.RR
}
// New returns a new and initialized *External.
func New() *External {
e := &External{hostmaster: "hostmaster", ttl: 5, apex: "dns"}
return e
}
// ServeDNS implements the plugin.Handle interface.
func (e *External) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
state := request.Request{W: w, Req: r}
zone := plugin.Zones(e.Zones).Matches(state.Name())
if zone == "" {
return plugin.NextOrFailure(e.Name(), e.Next, ctx, w, r)
}
if e.externalFunc == nil {
return plugin.NextOrFailure(e.Name(), e.Next, ctx, w, r)
}
state.Zone = zone
for _, z := range e.Zones {
// TODO(miek): save this in the External struct.
if state.Name() == z { // apex query
ret, err := e.serveApex(state)
return ret, err
}
if dns.IsSubDomain(e.apex+"."+z, state.Name()) {
// dns subdomain test for ns. and dns. queries
ret, err := e.serveSubApex(state)
return ret, err
}
}
svc, rcode := e.externalFunc(state)
m := new(dns.Msg)
m.SetReply(state.Req)
if len(svc) == 0 {
m.Rcode = rcode
m.Ns = []dns.RR{e.soa(state)}
w.WriteMsg(m)
return 0, nil
}
switch state.QType() {
case dns.TypeA:
m.Answer = e.a(svc, state)
case dns.TypeAAAA:
m.Answer = e.aaaa(svc, state)
case dns.TypeSRV:
m.Answer, m.Extra = e.srv(svc, state)
default:
m.Ns = []dns.RR{e.soa(state)}
}
// If we did have records, but queried for the wrong qtype return a nodata response.
if len(m.Answer) == 0 {
m.Ns = []dns.RR{e.soa(state)}
}
w.WriteMsg(m)
return 0, nil
}
// Name implements the Handler interface.
func (e *External) Name() string { return "k8s_external" }

View file

@ -0,0 +1,210 @@
package external
import (
"context"
"testing"
"github.com/coredns/coredns/plugin/kubernetes"
"github.com/coredns/coredns/plugin/kubernetes/object"
"github.com/coredns/coredns/plugin/pkg/dnstest"
"github.com/coredns/coredns/plugin/pkg/watch"
"github.com/coredns/coredns/plugin/test"
"github.com/coredns/coredns/request"
"github.com/miekg/dns"
api "k8s.io/api/core/v1"
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestExternal(t *testing.T) {
k := kubernetes.New([]string{"cluster.local."})
k.Namespaces = map[string]struct{}{"testns": struct{}{}}
k.APIConn = &external{}
e := New()
e.Zones = []string{"example.com."}
e.Next = test.NextHandler(dns.RcodeSuccess, nil)
e.externalFunc = k.External
e.externalAddrFunc = externalAddress // internal test function
ctx := context.TODO()
for i, tc := range tests {
r := tc.Msg()
w := dnstest.NewRecorder(&test.ResponseWriter{})
_, err := e.ServeDNS(ctx, w, r)
if err != tc.Error {
t.Errorf("Test %d expected no error, got %v", i, err)
return
}
if tc.Error != nil {
continue
}
resp := w.Msg
if resp == nil {
t.Fatalf("Test %d, got nil message and no error for %q", i, r.Question[0].Name)
}
test.SortAndCheck(t, resp, tc)
}
}
var tests = []test.Case{
// A Service
{
Qname: "svc1.testns.example.com.", Qtype: dns.TypeA, Rcode: dns.RcodeSuccess,
Answer: []dns.RR{
test.A("svc1.testns.example.com. 5 IN A 1.2.3.4"),
},
},
{
Qname: "svc1.testns.example.com.", Qtype: dns.TypeSRV, Rcode: dns.RcodeSuccess,
Answer: []dns.RR{test.SRV("svc1.testns.example.com. 5 IN SRV 0 100 80 svc1.testns.example.com.")},
Extra: []dns.RR{test.A("svc1.testns.example.com. 5 IN A 1.2.3.4")},
},
// SRV Service Not udp/tcp
{
Qname: "*._not-udp-or-tcp.svc1.testns.example.com.", Qtype: dns.TypeSRV, Rcode: dns.RcodeNameError,
Ns: []dns.RR{
test.SOA("example.com. 5 IN SOA ns1.dns.example.com. hostmaster.example.com. 1499347823 7200 1800 86400 5"),
},
},
// SRV Service
{
Qname: "_http._tcp.svc1.testns.example.com.", Qtype: dns.TypeSRV, Rcode: dns.RcodeSuccess,
Answer: []dns.RR{
test.SRV("_http._tcp.svc1.testns.example.com. 5 IN SRV 0 100 80 svc1.testns.example.com."),
},
Extra: []dns.RR{
test.A("svc1.testns.example.com. 5 IN A 1.2.3.4"),
},
},
// AAAA Service (with an existing A record, but no AAAA record)
{
Qname: "svc1.testns.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"),
},
},
// AAAA Service (non-existing service)
{
Qname: "svc0.testns.example.com.", Qtype: dns.TypeAAAA, Rcode: dns.RcodeNameError,
Ns: []dns.RR{
test.SOA("example.com. 5 IN SOA ns1.dns.example.com. hostmaster.example.com. 1499347823 7200 1800 86400 5"),
},
},
// A Service (non-existing service)
{
Qname: "svc0.testns.example.com.", Qtype: dns.TypeA, Rcode: dns.RcodeNameError,
Ns: []dns.RR{
test.SOA("example.com. 5 IN SOA ns1.dns.example.com. hostmaster.example.com. 1499347823 7200 1800 86400 5"),
},
},
// A Service (non-existing namespace)
{
Qname: "svc0.svc-nons.example.com.", Qtype: dns.TypeA, Rcode: dns.RcodeNameError,
Ns: []dns.RR{
test.SOA("example.com. 5 IN SOA ns1.dns.example.com. hostmaster.example.com. 1499347823 7200 1800 86400 5"),
},
},
// AAAA Service
{
Qname: "svc6.testns.example.com.", Qtype: dns.TypeAAAA, Rcode: dns.RcodeSuccess,
Answer: []dns.RR{
test.AAAA("svc6.testns.example.com. 5 IN AAAA 1:2::5"),
},
},
// SRV
{
Qname: "_http._tcp.svc6.testns.example.com.", Qtype: dns.TypeSRV, Rcode: dns.RcodeSuccess,
Answer: []dns.RR{
test.SRV("_http._tcp.svc6.testns.example.com. 5 IN SRV 0 100 80 svc6.testns.example.com."),
},
Extra: []dns.RR{
test.AAAA("svc6.testns.example.com. 5 IN AAAA 1:2::5"),
},
},
// SRV
{
Qname: "svc6.testns.example.com.", Qtype: dns.TypeSRV, Rcode: dns.RcodeSuccess,
Answer: []dns.RR{
test.SRV("svc6.testns.example.com. 5 IN SRV 0 100 80 svc6.testns.example.com."),
},
Extra: []dns.RR{
test.AAAA("svc6.testns.example.com. 5 IN AAAA 1:2::5"),
},
},
{
Qname: "testns.example.com.", Qtype: dns.TypeA, 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"),
},
},
{
Qname: "testns.example.com.", Qtype: dns.TypeSOA, 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"),
},
},
}
type external struct{}
func (external) HasSynced() bool { return true }
func (external) Run() { return }
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) SetWatchChan(watch.Chan) {}
func (external) Watch(string) error { return nil }
func (external) StopWatching(string) {}
func (external) EpIndex(s string) []*object.Endpoints { return nil }
func (external) EndpointsList() []*object.Endpoints { return nil }
func (external) GetNodeByName(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 }
func (external) GetNamespaceByName(name string) (*api.Namespace, error) {
return &api.Namespace{
ObjectMeta: meta.ObjectMeta{
Name: name,
},
}, nil
}
var svcIndexExternal = map[string][]*object.Service{
"svc1.testns": {
{
Name: "svc1",
Namespace: "testns",
Type: api.ServiceTypeClusterIP,
ClusterIP: "10.0.0.1",
ExternalIPs: []string{"1.2.3.4"},
Ports: []api.ServicePort{{Name: "http", Protocol: "tcp", Port: 80}},
},
},
"svc6.testns": {
{
Name: "svc6",
Namespace: "testns",
Type: api.ServiceTypeClusterIP,
ClusterIP: "10.0.0.3",
ExternalIPs: []string{"1:2::5"},
Ports: []api.ServicePort{{Name: "http", Protocol: "tcp", Port: 80}},
},
},
}
func (external) ServiceList() []*object.Service {
var svcs []*object.Service
for _, svc := range svcIndexExternal {
svcs = append(svcs, svc...)
}
return svcs
}
func externalAddress(state request.Request) []dns.RR {
a := test.A("example.org. IN A 127.0.0.1")
return []dns.RR{a}
}

View file

@ -0,0 +1,148 @@
package external
import (
"math"
"github.com/coredns/coredns/plugin/etcd/msg"
"github.com/coredns/coredns/request"
"github.com/miekg/dns"
)
func (e *External) a(services []msg.Service, state request.Request) (records []dns.RR) {
dup := make(map[string]struct{})
for _, s := range services {
what, ip := s.HostType()
switch what {
case dns.TypeCNAME:
// can't happen
case dns.TypeA:
if _, ok := dup[s.Host]; !ok {
dup[s.Host] = struct{}{}
rr := s.NewA(state.QName(), ip)
rr.Hdr.Ttl = e.ttl
records = append(records, rr)
}
case dns.TypeAAAA:
// nada
}
}
return records
}
func (e *External) aaaa(services []msg.Service, state request.Request) (records []dns.RR) {
dup := make(map[string]struct{})
for _, s := range services {
what, ip := s.HostType()
switch what {
case dns.TypeCNAME:
// can't happen
case dns.TypeA:
// nada
case dns.TypeAAAA:
if _, ok := dup[s.Host]; !ok {
dup[s.Host] = struct{}{}
rr := s.NewAAAA(state.QName(), ip)
rr.Hdr.Ttl = e.ttl
records = append(records, rr)
}
}
}
return records
}
func (e *External) srv(services []msg.Service, state request.Request) (records, extra []dns.RR) {
dup := make(map[item]struct{})
// Looping twice to get the right weight vs priority. This might break because we may drop duplicate SRV records latter on.
w := make(map[int]int)
for _, s := range services {
weight := 100
if s.Weight != 0 {
weight = s.Weight
}
if _, ok := w[s.Priority]; !ok {
w[s.Priority] = weight
continue
}
w[s.Priority] += weight
}
for _, s := range services {
// Don't add the entry if the port is -1 (invalid). The kubernetes plugin uses port -1 when a service/endpoint
// does not have any declared ports.
if s.Port == -1 {
continue
}
w1 := 100.0 / float64(w[s.Priority])
if s.Weight == 0 {
w1 *= 100
} else {
w1 *= float64(s.Weight)
}
weight := uint16(math.Floor(w1))
what, ip := s.HostType()
switch what {
case dns.TypeCNAME:
// can't happen
case dns.TypeA, dns.TypeAAAA:
addr := s.Host
s.Host = msg.Domain(s.Key)
srv := s.NewSRV(state.QName(), weight)
if ok := isDuplicate(dup, srv.Target, "", srv.Port); !ok {
records = append(records, srv)
}
if ok := isDuplicate(dup, srv.Target, addr, 0); !ok {
hdr := dns.RR_Header{Name: srv.Target, Rrtype: what, Class: dns.ClassINET, Ttl: e.ttl}
switch what {
case dns.TypeA:
extra = append(extra, &dns.A{Hdr: hdr, A: ip})
case dns.TypeAAAA:
extra = append(extra, &dns.AAAA{Hdr: hdr, AAAA: ip})
}
}
}
}
return records, extra
}
// not sure if this is even needed.
// item holds records.
type item struct {
name string // name of the record (either owner or something else unique).
port uint16 // port of the record (used for address records, A and AAAA).
addr string // address of the record (A and AAAA).
}
// isDuplicate uses m to see if the combo (name, addr, port) already exists. If it does
// not exist already IsDuplicate will also add the record to the map.
func isDuplicate(m map[item]struct{}, name, addr string, port uint16) bool {
if addr != "" {
_, ok := m[item{name, 0, addr}]
if !ok {
m[item{name, 0, addr}] = struct{}{}
}
return ok
}
_, ok := m[item{name, port, ""}]
if !ok {
m[item{name, port, ""}] = struct{}{}
}
return ok
}

View file

@ -0,0 +1,86 @@
package external
import (
"strconv"
"github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/plugin"
"github.com/mholt/caddy"
)
func init() {
caddy.RegisterPlugin("k8s_external", caddy.Plugin{
ServerType: "dns",
Action: setup,
})
}
func setup(c *caddy.Controller) error {
e, err := parse(c)
if err != nil {
return plugin.Error("k8s_external", err)
}
// Do this in OnStartup, so all plugins have been initialized.
c.OnStartup(func() error {
m := dnsserver.GetConfig(c).Handler("kubernetes")
if m == nil {
return nil
}
if x, ok := m.(Externaler); ok {
e.externalFunc = x.External
e.externalAddrFunc = x.ExternalAddress
}
return nil
})
dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
e.Next = next
return e
})
return nil
}
func parse(c *caddy.Controller) (*External, error) {
e := New()
for c.Next() { // external
zones := c.RemainingArgs()
e.Zones = zones
if len(zones) == 0 {
e.Zones = make([]string, len(c.ServerBlockKeys))
copy(e.Zones, c.ServerBlockKeys)
}
for i, str := range e.Zones {
e.Zones[i] = plugin.Host(str).Normalize()
}
for c.NextBlock() {
switch c.Val() {
case "ttl":
args := c.RemainingArgs()
if len(args) == 0 {
return nil, c.ArgErr()
}
t, err := strconv.Atoi(args[0])
if err != nil {
return nil, err
}
if t < 0 || t > 3600 {
return nil, c.Errf("ttl must be in range [0, 3600]: %d", t)
}
e.ttl = uint32(t)
case "apex":
args := c.RemainingArgs()
if len(args) == 0 {
return nil, c.ArgErr()
}
e.apex = args[0]
default:
return nil, c.Errf("unknown property '%s'", c.Val())
}
}
}
return e, nil
}

View file

@ -0,0 +1,48 @@
package external
import (
"testing"
"github.com/mholt/caddy"
)
func TestSetup(t *testing.T) {
tests := []struct {
input string
shouldErr bool
expectedZone string
expectedApex string
}{
{`k8s_external`, false, "", "dns"},
{`k8s_external example.org`, false, "example.org.", "dns"},
{`k8s_external example.org {
apex testdns
}`, false, "example.org.", "testdns"},
}
for i, test := range tests {
c := caddy.NewTestController("dns", test.input)
e, err := parse(c)
if test.shouldErr && err == nil {
t.Errorf("Test %d: Expected error but found %s for input %s", i, err, test.input)
}
if err != nil {
if !test.shouldErr {
t.Errorf("Test %d: Expected no error but found one for input %s. Error was: %v", i, test.input, err)
}
}
if !test.shouldErr && test.expectedZone != "" {
if test.expectedZone != e.Zones[0] {
t.Errorf("Test %d, expected zone %q for input %s, got: %q", i, test.expectedZone, test.input, e.Zones[0])
}
}
if !test.shouldErr {
if test.expectedApex != e.apex {
t.Errorf("Test %d, expected apex %q for input %s, got: %q", i, test.expectedApex, test.input, e.apex)
}
}
}
}

View file

@ -172,9 +172,13 @@ func svcIPIndexFunc(obj interface{}) ([]string, error) {
if !ok {
return nil, errObj
}
if len(svc.ExternalIPs) == 0 {
return []string{svc.ClusterIP}, nil
}
return append([]string{svc.ClusterIP}, svc.ExternalIPs...), nil
}
func svcNameNamespaceIndexFunc(obj interface{}) ([]string, error) {
s, ok := obj.(*object.Service)
if !ok {

View file

@ -0,0 +1,92 @@
package kubernetes
import (
"strings"
"github.com/coredns/coredns/plugin/etcd/msg"
"github.com/coredns/coredns/plugin/kubernetes/object"
"github.com/coredns/coredns/plugin/pkg/dnsutil"
"github.com/coredns/coredns/request"
"github.com/miekg/dns"
)
// External implements the ExternalFunc call from the external plugin.
// It returns any services matching in the services' ExternalIPs.
func (k *Kubernetes) External(state request.Request) ([]msg.Service, int) {
base, _ := dnsutil.TrimZone(state.Name(), state.Zone)
segs := dns.SplitDomainName(base)
last := len(segs) - 1
if last < 0 {
return nil, dns.RcodeServerFailure
}
// We dealing with a fairly normal domain name here, but; we still need to have the service
// and the namespace:
// service.namespace.<base>
//
// for service (and SRV) you can also say _tcp, and port (i.e. _http), we need those be picked
// up, unless they are not specified, then we use an internal wildcard.
port := "*"
protocol := "*"
namespace := segs[last]
if !k.namespaceExposed(namespace) || !k.namespace(namespace) {
return nil, dns.RcodeNameError
}
last--
if last < 0 {
return nil, dns.RcodeSuccess
}
service := segs[last]
last--
if last == 1 {
protocol = stripUnderscore(segs[last])
port = stripUnderscore(segs[last-1])
last -= 2
}
if last != -1 {
// too long
return nil, dns.RcodeNameError
}
idx := object.ServiceKey(service, namespace)
serviceList := k.APIConn.SvcIndex(idx)
services := []msg.Service{}
zonePath := msg.Path(state.Zone, coredns)
rcode := dns.RcodeNameError
for _, svc := range serviceList {
if namespace != svc.Namespace {
continue
}
if service != svc.Name {
continue
}
for _, ip := range svc.ExternalIPs {
for _, p := range svc.Ports {
if !(match(port, p.Name) && match(protocol, string(p.Protocol))) {
continue
}
rcode = dns.RcodeSuccess
s := msg.Service{Host: ip, Port: int(p.Port), TTL: k.ttl}
s.Key = strings.Join([]string{zonePath, svc.Namespace, svc.Name}, "/")
services = append(services, s)
}
}
}
return services, rcode
}
// ExternalAddress returns the external service address(es) for the CoreDNS service.
func (k *Kubernetes) ExternalAddress(state request.Request) []dns.RR {
// This is probably wrong, because of all the fallback behavior of k.nsAddr, i.e. can get
// an address that isn't reacheable from outside the cluster.
rrs := []dns.RR{k.nsAddr()}
return rrs
}

View file

@ -0,0 +1,139 @@
package kubernetes
import (
"testing"
"github.com/coredns/coredns/plugin/etcd/msg"
"github.com/coredns/coredns/plugin/kubernetes/object"
"github.com/coredns/coredns/plugin/pkg/watch"
"github.com/coredns/coredns/plugin/test"
"github.com/coredns/coredns/request"
"github.com/miekg/dns"
api "k8s.io/api/core/v1"
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
)
var extCases = []struct {
Qname string
Qtype uint16
Msg []msg.Service
Rcode int
}{
{
Qname: "svc1.testns.example.org.", Rcode: dns.RcodeSuccess,
Msg: []msg.Service{
msg.Service{Host: "1.2.3.4", Port: 80, TTL: 5, Key: "/c/org/example/testns/svc1"},
},
},
{
Qname: "svc6.testns.example.org.", Rcode: dns.RcodeSuccess,
Msg: []msg.Service{
msg.Service{Host: "1:2::5", Port: 80, TTL: 5, Key: "/c/org/example/testns/svc1"},
},
},
{
Qname: "*._not-udp-or-tcp.svc1.testns.example.com.", Rcode: dns.RcodeSuccess,
},
{
Qname: "_http._tcp.svc1.testns.example.com.", Rcode: dns.RcodeSuccess,
Msg: []msg.Service{
msg.Service{Host: "1.2.3.4", Port: 80, TTL: 5, Key: "/c/org/example/testns/svc1"},
},
},
{
Qname: "svc0.testns.example.com.", Rcode: dns.RcodeNameError,
},
{
Qname: "svc0.svc-nons.example.com.", Rcode: dns.RcodeNameError,
},
}
func TestExternal(t *testing.T) {
k := New([]string{"cluster.local."})
k.APIConn = &external{}
k.Next = test.NextHandler(dns.RcodeSuccess, nil)
k.Namespaces = map[string]struct{}{"testns": struct{}{}}
for i, tc := range extCases {
state := testRequest(tc.Qname)
svc, rcode := k.External(state)
if x := tc.Rcode; x != rcode {
t.Errorf("Test %d, expected rcode %d, got %d\n", i, x, rcode)
}
if len(svc) != len(tc.Msg) {
t.Errorf("Test %d, expected %d for messages, got %d\n", i, len(tc.Msg), len(svc))
}
for j, s := range svc {
if x := tc.Msg[j].Key; x != s.Key {
t.Errorf("Test %d, expected key %s, got %s\n", i, x, s.Key)
}
return
}
}
}
type external struct{}
func (external) HasSynced() bool { return true }
func (external) Run() { return }
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) SetWatchChan(watch.Chan) {}
func (external) Watch(string) error { return nil }
func (external) StopWatching(string) {}
func (external) EpIndex(s string) []*object.Endpoints { return nil }
func (external) EndpointsList() []*object.Endpoints { return nil }
func (external) GetNodeByName(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 }
func (external) GetNamespaceByName(name string) (*api.Namespace, error) {
return &api.Namespace{
ObjectMeta: meta.ObjectMeta{
Name: name,
},
}, nil
}
var svcIndexExternal = map[string][]*object.Service{
"svc1.testns": {
{
Name: "svc1",
Namespace: "testns",
Type: api.ServiceTypeClusterIP,
ClusterIP: "10.0.0.1",
ExternalIPs: []string{"1.2.3.4"},
Ports: []api.ServicePort{{Name: "http", Protocol: "tcp", Port: 80}},
},
},
"svc6.testns": {
{
Name: "svc6",
Namespace: "testns",
Type: api.ServiceTypeClusterIP,
ClusterIP: "10.0.0.3",
ExternalIPs: []string{"1:2::5"},
Ports: []api.ServicePort{{Name: "http", Protocol: "tcp", Port: 80}},
},
},
}
func (external) ServiceList() []*object.Service {
var svcs []*object.Service
for _, svc := range svcIndexExternal {
svcs = append(svcs, svc...)
}
return svcs
}
func testRequest(name string) request.Request {
m := new(dns.Msg).SetQuestion(name, dns.TypeA)
return request.Request{W: &test.ResponseWriter{}, Req: m, Zone: "example.org."}
}

View file

@ -12,6 +12,10 @@ func isDefaultNS(name, zone string) bool {
return strings.Index(name, defaultNSName) == 0 && strings.Index(name, zone) == len(defaultNSName)
}
// nsAddr return the A record for the CoreDNS service in the cluster. If it fails that it fallsback
// on the local address of the machine we're running on.
//
// This function is rather expensive to run.
func (k *Kubernetes) nsAddr() *dns.A {
var (
svcName string

View file

@ -16,6 +16,9 @@ type Service struct {
ExternalName string
Ports []api.ServicePort
// ExternalIPs we may want to export.
ExternalIPs []string
*Empty
}
@ -37,6 +40,8 @@ func ToService(obj interface{}) interface{} {
ClusterIP: svc.Spec.ClusterIP,
Type: svc.Spec.Type,
ExternalName: svc.Spec.ExternalName,
ExternalIPs: make([]string, len(svc.Status.LoadBalancer.Ingress)+len(svc.Spec.ExternalIPs)),
}
if len(svc.Spec.Ports) == 0 {
@ -47,6 +52,11 @@ func ToService(obj interface{}) interface{} {
copy(s.Ports, svc.Spec.Ports)
}
li := copy(s.ExternalIPs, svc.Spec.ExternalIPs)
for i, lb := range svc.Status.LoadBalancer.Ingress {
s.ExternalIPs[li+i] = lb.IP
}
*svc = api.Service{}
return s
@ -65,8 +75,10 @@ func (s *Service) DeepCopyObject() runtime.Object {
Type: s.Type,
ExternalName: s.ExternalName,
Ports: make([]api.ServicePort, len(s.Ports)),
ExternalIPs: make([]string, len(s.ExternalIPs)),
}
copy(s1.Ports, s.Ports)
copy(s1.ExternalIPs, s.ExternalIPs)
return s1
}