mirror of
https://github.com/coredns/coredns.git
synced 2025-11-16 00:42:16 -05:00
mw/federation: add federation back as separate mw for k8s (#929)
* mw/federaration This PR add the federation back as a middleware to keep it more contained from the main kubernetes code. It also makes parseRequest less import and pushes this functionlity down in the k.Entries. This minimizes (or tries to) the importance for the qtype in the query. In the end the qtype checking should only happen in ServeDNS - but for k8s this might proof difficult. Numerous other cleanup in code and kubernetes tests. * up test coverage
This commit is contained in:
43
middleware/federation/README.md
Normal file
43
middleware/federation/README.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# federation
|
||||
|
||||
The *federation* middleware enables
|
||||
[federated](https://kubernetes.io/docs/tasks/federation/federation-service-discovery/) queries to be
|
||||
resolved via the kubernetes middleware.
|
||||
|
||||
Enabling *federation* without also having *kubernetes* is a noop.
|
||||
|
||||
## Syntax
|
||||
|
||||
~~~
|
||||
federation [ZONES...] {
|
||||
NAME DOMAIN
|
||||
~~~
|
||||
|
||||
* Each **NAME** and **DOMAIN** defines federation membership. One entry for each. A duplicate
|
||||
**NAME** will silently overwrite any previous value.
|
||||
|
||||
## Examples
|
||||
|
||||
Here we handle all service requests in the `prod` and `stage` federations.
|
||||
|
||||
~~~ txt
|
||||
. {
|
||||
kubernetes cluster.local
|
||||
federation cluster.local {
|
||||
prod prod.feddomain.com
|
||||
staging staging.feddomain.com
|
||||
}
|
||||
}
|
||||
~~~
|
||||
|
||||
Or slightly shorter:
|
||||
|
||||
~~~ txt
|
||||
cluster.local {
|
||||
kubernetes
|
||||
federation {
|
||||
prod prod.feddomain.com
|
||||
staging staging.feddomain.com
|
||||
}
|
||||
}
|
||||
~~~
|
||||
140
middleware/federation/federation.go
Normal file
140
middleware/federation/federation.go
Normal file
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
Package federation implements kubernetes federation. It checks if the qname matches
|
||||
a possible federation. If this is the case and the captured answer is an NXDOMAIN,
|
||||
federation is performed. If this is not the case the original answer is returned.
|
||||
|
||||
The federation label is always the 2nd to last once the zone is chopped of. For
|
||||
instance "nginx.mynamespace.myfederation.svc.example.com" has "myfederation" as
|
||||
the federation label. For federation to work we do a normal k8s lookup
|
||||
*without* that label, if that comes back with NXDOMAIN or NODATA(??) we create
|
||||
a federation record and return that.
|
||||
|
||||
Federation is only useful in conjunction with the kubernetes middleware, without it is a noop.
|
||||
*/
|
||||
package federation
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/coredns/coredns/middleware"
|
||||
"github.com/coredns/coredns/middleware/etcd/msg"
|
||||
"github.com/coredns/coredns/middleware/pkg/dnsutil"
|
||||
"github.com/coredns/coredns/request"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// Federation contains the name to zone mapping used for federation in kubernetes.
|
||||
type Federation struct {
|
||||
f map[string]string
|
||||
zones []string
|
||||
|
||||
Next middleware.Handler
|
||||
Federations Func
|
||||
}
|
||||
|
||||
// Func needs to be implemented by any middleware that implements
|
||||
// federation. Right now this is only the kubernetes middleware.
|
||||
type Func func(state request.Request, fname, fzone string) (msg.Service, error)
|
||||
|
||||
// New returns a new federation.
|
||||
func New() *Federation {
|
||||
return &Federation{f: make(map[string]string)}
|
||||
}
|
||||
|
||||
// ServeDNS implements the middleware.Handle interface.
|
||||
func (f *Federation) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
if f.Federations == nil {
|
||||
return middleware.NextOrFailure(f.Name(), f.Next, ctx, w, r)
|
||||
}
|
||||
|
||||
state := request.Request{W: w, Req: r}
|
||||
zone := middleware.Zones(f.zones).Matches(state.Name())
|
||||
if zone == "" {
|
||||
return middleware.NextOrFailure(f.Name(), f.Next, ctx, w, r)
|
||||
}
|
||||
|
||||
state.Zone = zone
|
||||
|
||||
// Remove the federation label from the qname to see if something exists.
|
||||
without, label := f.isNameFederation(state.Name(), state.Zone)
|
||||
if without == "" {
|
||||
return middleware.NextOrFailure(f.Name(), f.Next, ctx, w, r)
|
||||
}
|
||||
|
||||
qname := r.Question[0].Name
|
||||
r.Question[0].Name = without
|
||||
state.Clear()
|
||||
|
||||
// Start the next middleware, but with a nowriter, capture the result, if NXDOMAIN
|
||||
// perform federation, otherwise just write the result.
|
||||
nw := NewNonWriter(w)
|
||||
ret, err := middleware.NextOrFailure(f.Name(), f.Next, ctx, nw, r)
|
||||
|
||||
if !middleware.ClientWrite(ret) {
|
||||
// something went wrong
|
||||
return ret, err
|
||||
}
|
||||
|
||||
if m := nw.Msg; m.Rcode != dns.RcodeNameError {
|
||||
// If positive answer we need to substitute the orinal qname in question and answer.
|
||||
r.Question[0].Name = qname
|
||||
for _, a := range m.Answer {
|
||||
a.Header().Name = qname
|
||||
}
|
||||
|
||||
state.SizeAndDo(m)
|
||||
m, _ = state.Scrub(m)
|
||||
w.WriteMsg(m)
|
||||
|
||||
return dns.RcodeSuccess, nil
|
||||
}
|
||||
|
||||
// Still here, we've seen NXDOMAIN and need to perform federation.
|
||||
service, err := f.Federations(state, label, f.f[label]) // state references Req which has updated qname
|
||||
if err != nil {
|
||||
return dns.RcodeServerFailure, err
|
||||
}
|
||||
|
||||
r.Question[0].Name = qname
|
||||
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(r)
|
||||
m.Authoritative, m.RecursionAvailable, m.Compress = true, true, true
|
||||
|
||||
m.Answer = []dns.RR{service.NewCNAME(state.QName(), service.Host)}
|
||||
|
||||
state.SizeAndDo(m)
|
||||
m, _ = state.Scrub(m)
|
||||
w.WriteMsg(m)
|
||||
|
||||
return dns.RcodeSuccess, nil
|
||||
}
|
||||
|
||||
// Name implements the middleware.Handle interface.
|
||||
func (f *Federation) Name() string { return "federation" }
|
||||
|
||||
// IsNameFederation checks the qname to see if it is a potential federation. The federation
|
||||
// label is always the 2nd to last once the zone is chopped of. For instance
|
||||
// "nginx.mynamespace.myfederation.svc.example.com" has "myfederation" as the federation label.
|
||||
// IsNameFederation returns a new qname with the federation label and the label itself or two
|
||||
// emtpy strings if there wasn't a hit.
|
||||
func (f *Federation) isNameFederation(name, zone string) (string, string) {
|
||||
base, _ := dnsutil.TrimZone(name, zone)
|
||||
|
||||
// TODO(miek): dns.PrevLabel is better for memory, or dns.Split.
|
||||
labels := dns.SplitDomainName(base)
|
||||
ll := len(labels)
|
||||
if ll < 2 {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
fed := labels[ll-2]
|
||||
|
||||
if _, ok := f.f[fed]; ok {
|
||||
without := strings.Join(labels[:ll-2], ".") + "." + labels[ll-1] + "." + zone
|
||||
return without, fed
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
81
middleware/federation/federation_test.go
Normal file
81
middleware/federation/federation_test.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package federation
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/coredns/coredns/middleware/kubernetes"
|
||||
"github.com/coredns/coredns/middleware/pkg/dnsrecorder"
|
||||
"github.com/coredns/coredns/middleware/test"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func TestIsNameFederation(t *testing.T) {
|
||||
tests := []struct {
|
||||
fed string
|
||||
qname string
|
||||
expectedZone string
|
||||
}{
|
||||
{"prod", "nginx.mynamespace.prod.svc.example.com.", "nginx.mynamespace.svc.example.com."},
|
||||
{"prod", "nginx.mynamespace.staging.svc.example.com.", ""},
|
||||
{"prod", "nginx.mynamespace.example.com.", ""},
|
||||
{"prod", "example.com.", ""},
|
||||
{"prod", "com.", ""},
|
||||
}
|
||||
|
||||
fed := New()
|
||||
for i, tc := range tests {
|
||||
fed.f[tc.fed] = "test-name"
|
||||
if x, _ := fed.isNameFederation(tc.qname, "example.com."); x != tc.expectedZone {
|
||||
t.Errorf("Test %d, failed to get zone, expected %s, got %s", i, tc.expectedZone, x)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFederationKubernetes(t *testing.T) {
|
||||
tests := []test.Case{
|
||||
{
|
||||
// service exists so we return the IP address associated with it.
|
||||
Qname: "svc1.testns.prod.svc.cluster.local.", Qtype: dns.TypeA,
|
||||
Rcode: dns.RcodeSuccess,
|
||||
Answer: []dns.RR{
|
||||
test.A("svc1.testns.prod.svc.cluster.local. 303 IN A 10.0.0.1"),
|
||||
},
|
||||
},
|
||||
{
|
||||
// service does not exist, do the federation dance.
|
||||
Qname: "svc0.testns.prod.svc.cluster.local.", Qtype: dns.TypeA,
|
||||
Rcode: dns.RcodeSuccess,
|
||||
Answer: []dns.RR{
|
||||
test.CNAME("svc0.testns.prod.svc.cluster.local. 303 IN CNAME svc0.testns.prod.svc.fd-az.fd-r.federal.example."),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
k := kubernetes.New([]string{"cluster.local."})
|
||||
k.APIConn = &APIConnFederationTest{}
|
||||
|
||||
fed := New()
|
||||
fed.zones = []string{"cluster.local."}
|
||||
fed.Federations = k.Federations
|
||||
fed.Next = k
|
||||
fed.f = map[string]string{
|
||||
"prod": "federal.example.",
|
||||
}
|
||||
|
||||
ctx := context.TODO()
|
||||
for i, tc := range tests {
|
||||
m := tc.Msg()
|
||||
|
||||
rec := dnsrecorder.New(&test.ResponseWriter{})
|
||||
_, err := fed.ServeDNS(ctx, rec, m)
|
||||
if err != nil {
|
||||
t.Errorf("Test %d, expected no error, got %v\n", i, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := rec.Msg
|
||||
test.SortAndCheck(t, resp, tc)
|
||||
}
|
||||
}
|
||||
111
middleware/federation/kubernetes_api_test.go
Normal file
111
middleware/federation/kubernetes_api_test.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package federation
|
||||
|
||||
import (
|
||||
"github.com/coredns/coredns/middleware/kubernetes"
|
||||
|
||||
"k8s.io/client-go/1.5/pkg/api"
|
||||
)
|
||||
|
||||
type APIConnFederationTest struct{}
|
||||
|
||||
func (APIConnFederationTest) Run() { return }
|
||||
func (APIConnFederationTest) Stop() error { return nil }
|
||||
|
||||
func (APIConnFederationTest) PodIndex(string) []interface{} {
|
||||
a := make([]interface{}, 1)
|
||||
a[0] = &api.Pod{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Namespace: "podns",
|
||||
},
|
||||
Status: api.PodStatus{
|
||||
PodIP: "10.240.0.1", // Remote IP set in test.ResponseWriter
|
||||
},
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
func (APIConnFederationTest) ServiceList() []*api.Service {
|
||||
svcs := []*api.Service{
|
||||
{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "svc1",
|
||||
Namespace: "testns",
|
||||
},
|
||||
Spec: api.ServiceSpec{
|
||||
ClusterIP: "10.0.0.1",
|
||||
Ports: []api.ServicePort{{
|
||||
Name: "http",
|
||||
Protocol: "tcp",
|
||||
Port: 80,
|
||||
}},
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "hdls1",
|
||||
Namespace: "testns",
|
||||
},
|
||||
Spec: api.ServiceSpec{
|
||||
ClusterIP: api.ClusterIPNone,
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "external",
|
||||
Namespace: "testns",
|
||||
},
|
||||
Spec: api.ServiceSpec{
|
||||
ExternalName: "ext.interwebs.test",
|
||||
Ports: []api.ServicePort{{
|
||||
Name: "http",
|
||||
Protocol: "tcp",
|
||||
Port: 80,
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
return svcs
|
||||
|
||||
}
|
||||
|
||||
func (APIConnFederationTest) EndpointsList() api.EndpointsList {
|
||||
return api.EndpointsList{
|
||||
Items: []api.Endpoints{
|
||||
{
|
||||
Subsets: []api.EndpointSubset{
|
||||
{
|
||||
Addresses: []api.EndpointAddress{
|
||||
{
|
||||
IP: "172.0.0.1",
|
||||
Hostname: "ep1a",
|
||||
},
|
||||
},
|
||||
Ports: []api.EndpointPort{
|
||||
{
|
||||
Port: 80,
|
||||
Protocol: "tcp",
|
||||
Name: "http",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "svc1",
|
||||
Namespace: "testns",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (APIConnFederationTest) GetNodeByName(name string) (api.Node, error) {
|
||||
return api.Node{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "test.node.foo.bar",
|
||||
Labels: map[string]string{
|
||||
kubernetes.LabelRegion: "fd-r",
|
||||
kubernetes.LabelZone: "fd-az",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
22
middleware/federation/nonwriter.go
Normal file
22
middleware/federation/nonwriter.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package federation
|
||||
|
||||
import (
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// NonWriter is a type of ResponseWriter that captures the message, but never writes to the client.
|
||||
type NonWriter struct {
|
||||
dns.ResponseWriter
|
||||
Msg *dns.Msg
|
||||
}
|
||||
|
||||
// NewNonWriter makes and returns a new NonWriter.
|
||||
func NewNonWriter(w dns.ResponseWriter) *NonWriter { return &NonWriter{ResponseWriter: w} }
|
||||
|
||||
// WriteMsg records the message, but doesn't write it itself.
|
||||
func (r *NonWriter) WriteMsg(res *dns.Msg) error {
|
||||
r.Msg = res
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *NonWriter) Write(buf []byte) (int, error) { return len(buf), nil }
|
||||
81
middleware/federation/setup.go
Normal file
81
middleware/federation/setup.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package federation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/coredns/coredns/core/dnsserver"
|
||||
"github.com/coredns/coredns/middleware"
|
||||
"github.com/coredns/coredns/middleware/kubernetes"
|
||||
"github.com/miekg/dns"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterPlugin("federation", caddy.Plugin{
|
||||
ServerType: "dns",
|
||||
Action: setup,
|
||||
})
|
||||
}
|
||||
|
||||
func setup(c *caddy.Controller) error {
|
||||
fed, err := federationParse(c)
|
||||
if err != nil {
|
||||
return middleware.Error("federation", err)
|
||||
}
|
||||
|
||||
// Do this in OnStartup, so all middleware has been initialized.
|
||||
c.OnStartup(func() error {
|
||||
m := dnsserver.GetConfig(c).GetHandler("kubernetes")
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
if x, ok := m.(kubernetes.Kubernetes); ok {
|
||||
fed.Federations = x.Federations
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
dnsserver.GetConfig(c).AddMiddleware(func(next middleware.Handler) middleware.Handler {
|
||||
fed.Next = next
|
||||
return nil
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func federationParse(c *caddy.Controller) (*Federation, error) {
|
||||
fed := New()
|
||||
|
||||
for c.Next() {
|
||||
// federation [zones..]
|
||||
origins := make([]string, len(c.ServerBlockKeys))
|
||||
copy(origins, c.ServerBlockKeys)
|
||||
|
||||
for c.NextBlock() {
|
||||
x := c.Val()
|
||||
switch c.Val() {
|
||||
default:
|
||||
args := c.RemainingArgs()
|
||||
if len(args) != 1 {
|
||||
return fed, fmt.Errorf("need two arguments for federation: %q", args)
|
||||
}
|
||||
fed.f[x] = dns.Fqdn(args[0])
|
||||
}
|
||||
}
|
||||
|
||||
for i := range origins {
|
||||
origins[i] = middleware.Host(origins[i]).Normalize()
|
||||
}
|
||||
|
||||
fed.zones = origins
|
||||
|
||||
if len(fed.f) == 0 {
|
||||
return fed, fmt.Errorf("at least one name to zone federation expected")
|
||||
}
|
||||
|
||||
return fed, nil
|
||||
}
|
||||
|
||||
return fed, nil
|
||||
}
|
||||
60
middleware/federation/setup_test.go
Normal file
60
middleware/federation/setup_test.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package federation
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
)
|
||||
|
||||
func TestSetup(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
expectedLen int
|
||||
expectedNameZone []string // contains only entry for now
|
||||
}{
|
||||
{`federation {
|
||||
prod prod.example.org
|
||||
}`, false, 1, []string{"prod", "prod.example.org."}},
|
||||
|
||||
{`federation {
|
||||
staging staging.example.org
|
||||
prod prod.example.org
|
||||
}`, false, 2, []string{"prod", "prod.example.org."}},
|
||||
{`federation {
|
||||
staging staging.example.org
|
||||
prod prod.example.org
|
||||
}`, false, 2, []string{"staging", "staging.example.org."}},
|
||||
// errors
|
||||
{`federation {
|
||||
}`, true, 0, []string{}},
|
||||
{`federation {
|
||||
staging
|
||||
}`, true, 0, []string{}},
|
||||
}
|
||||
for i, test := range tests {
|
||||
c := caddy.NewTestController("dns", test.input)
|
||||
fed, err := federationParse(c)
|
||||
if test.shouldErr && err == nil {
|
||||
t.Errorf("Test %v: Expected error but found nil", i)
|
||||
continue
|
||||
} else if !test.shouldErr && err != nil {
|
||||
t.Errorf("Test %v: Expected no error but found error: %v", i, err)
|
||||
continue
|
||||
}
|
||||
if test.shouldErr && err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if x := len(fed.f); x != test.expectedLen {
|
||||
t.Errorf("Test %v: Expected map length of %d, got: %d", i, test.expectedLen, x)
|
||||
}
|
||||
if x, ok := fed.f[test.expectedNameZone[0]]; !ok {
|
||||
t.Errorf("Test %v: Expected name for %s, got nothing", i, test.expectedNameZone[0])
|
||||
} else {
|
||||
if x != test.expectedNameZone[1] {
|
||||
t.Errorf("Test %v: Expected zone: %s, got %s", i, test.expectedNameZone[1], x)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user