aboutsummaryrefslogtreecommitdiff
path: root/plugin/federation
diff options
context:
space:
mode:
Diffstat (limited to 'plugin/federation')
-rw-r--r--plugin/federation/README.md43
-rw-r--r--plugin/federation/federation.go141
-rw-r--r--plugin/federation/federation_test.go81
-rw-r--r--plugin/federation/kubernetes_api_test.go111
-rw-r--r--plugin/federation/setup.go89
-rw-r--r--plugin/federation/setup_test.go65
6 files changed, 530 insertions, 0 deletions
diff --git a/plugin/federation/README.md b/plugin/federation/README.md
new file mode 100644
index 000000000..fb3d44e8c
--- /dev/null
+++ b/plugin/federation/README.md
@@ -0,0 +1,43 @@
+# federation
+
+The *federation* plugin enables
+[federated](https://kubernetes.io/docs/tasks/federation/federation-service-discovery/) queries to be
+resolved via the kubernetes plugin.
+
+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
+ }
+}
+~~~
diff --git a/plugin/federation/federation.go b/plugin/federation/federation.go
new file mode 100644
index 000000000..c94e8f819
--- /dev/null
+++ b/plugin/federation/federation.go
@@ -0,0 +1,141 @@
+/*
+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 plugin, without it is a noop.
+*/
+package federation
+
+import (
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/etcd/msg"
+ "github.com/coredns/coredns/plugin/pkg/dnsutil"
+ "github.com/coredns/coredns/plugin/pkg/nonwriter"
+ "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 plugin.Handler
+ Federations Func
+}
+
+// Func needs to be implemented by any plugin that implements
+// federation. Right now this is only the kubernetes plugin.
+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 plugin.Handle interface.
+func (f *Federation) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
+ if f.Federations == nil {
+ return plugin.NextOrFailure(f.Name(), f.Next, ctx, w, r)
+ }
+
+ state := request.Request{W: w, Req: r}
+ zone := plugin.Zones(f.zones).Matches(state.Name())
+ if zone == "" {
+ return plugin.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 plugin.NextOrFailure(f.Name(), f.Next, ctx, w, r)
+ }
+
+ qname := r.Question[0].Name
+ r.Question[0].Name = without
+ state.Clear()
+
+ // Start the next plugin, but with a nowriter, capture the result, if NXDOMAIN
+ // perform federation, otherwise just write the result.
+ nw := nonwriter.New(w)
+ ret, err := plugin.NextOrFailure(f.Name(), f.Next, ctx, nw, r)
+
+ if !plugin.ClientWrite(ret) {
+ // something went wrong
+ r.Question[0].Name = qname
+ return ret, err
+ }
+
+ if m := nw.Msg; m.Rcode != dns.RcodeNameError {
+ // If positive answer we need to substitute the original qname in the answer.
+ m.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 {
+ r.Question[0].Name = qname
+ 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 plugin.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
+// empty 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 := dnsutil.Join(labels[:ll-2]) + labels[ll-1] + "." + zone
+ return without, fed
+ }
+ return "", ""
+}
diff --git a/plugin/federation/federation_test.go b/plugin/federation/federation_test.go
new file mode 100644
index 000000000..920f1a340
--- /dev/null
+++ b/plugin/federation/federation_test.go
@@ -0,0 +1,81 @@
+package federation
+
+import (
+ "testing"
+
+ "github.com/coredns/coredns/plugin/kubernetes"
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/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)
+ }
+}
diff --git a/plugin/federation/kubernetes_api_test.go b/plugin/federation/kubernetes_api_test.go
new file mode 100644
index 000000000..48a03666e
--- /dev/null
+++ b/plugin/federation/kubernetes_api_test.go
@@ -0,0 +1,111 @@
+package federation
+
+import (
+ "github.com/coredns/coredns/plugin/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
+}
diff --git a/plugin/federation/setup.go b/plugin/federation/setup.go
new file mode 100644
index 000000000..72514fe8f
--- /dev/null
+++ b/plugin/federation/setup.go
@@ -0,0 +1,89 @@
+package federation
+
+import (
+ "fmt"
+
+ "github.com/coredns/coredns/core/dnsserver"
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/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 plugin.Error("federation", err)
+ }
+
+ // Do this in OnStartup, so all plugin has been initialized.
+ c.OnStartup(func() error {
+ m := dnsserver.GetConfig(c).Handler("kubernetes")
+ if m == nil {
+ return nil
+ }
+ if x, ok := m.(*kubernetes.Kubernetes); ok {
+ fed.Federations = x.Federations
+ }
+ return nil
+ })
+
+ dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
+ fed.Next = next
+ return fed
+ })
+
+ return nil
+}
+
+func federationParse(c *caddy.Controller) (*Federation, error) {
+ fed := New()
+
+ for c.Next() {
+ // federation [zones..]
+ zones := c.RemainingArgs()
+ origins := []string{}
+ if len(zones) > 0 {
+ origins = make([]string, len(zones))
+ copy(origins, zones)
+ } else {
+ origins = make([]string, len(c.ServerBlockKeys))
+ copy(origins, c.ServerBlockKeys)
+ }
+
+ for c.NextBlock() {
+ x := c.Val()
+ switch x {
+ default:
+ args := c.RemainingArgs()
+ if x := len(args); x != 1 {
+ return fed, fmt.Errorf("need two arguments for federation, got %d", x)
+ }
+
+ fed.f[x] = dns.Fqdn(args[0])
+ }
+ }
+
+ for i := range origins {
+ origins[i] = plugin.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
+}
diff --git a/plugin/federation/setup_test.go b/plugin/federation/setup_test.go
new file mode 100644
index 000000000..e85b01772
--- /dev/null
+++ b/plugin/federation/setup_test.go
@@ -0,0 +1,65 @@
+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
+ }{
+ // ok
+ {`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."}},
+ {`federation example.com {
+ 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)
+ }
+ }
+ }
+}