diff --git a/core/plugin/zplugin.go b/core/plugin/zplugin.go index 104a3e68a..e761f8586 100644 --- a/core/plugin/zplugin.go +++ b/core/plugin/zplugin.go @@ -20,7 +20,6 @@ import ( _ "github.com/coredns/coredns/plugin/erratic" _ "github.com/coredns/coredns/plugin/errors" _ "github.com/coredns/coredns/plugin/etcd" - _ "github.com/coredns/coredns/plugin/federation" _ "github.com/coredns/coredns/plugin/file" _ "github.com/coredns/coredns/plugin/forward" _ "github.com/coredns/coredns/plugin/grpc" @@ -45,4 +44,5 @@ import ( _ "github.com/coredns/coredns/plugin/tls" _ "github.com/coredns/coredns/plugin/trace" _ "github.com/coredns/coredns/plugin/whoami" + _ "github.com/coredns/federation" ) diff --git a/go.mod b/go.mod index 99828467b..2632ccd07 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/Shopify/sarama v1.21.0 // indirect github.com/aws/aws-sdk-go v1.22.3 github.com/caddyserver/caddy v1.0.1 + github.com/coredns/federation v0.0.0-20190818181423-e032b096babe github.com/coreos/bbolt v1.3.2 // indirect github.com/coreos/etcd v3.3.13+incompatible github.com/coreos/go-semver v0.2.0 // indirect diff --git a/go.sum b/go.sum index 860fb298f..aad90f981 100644 --- a/go.sum +++ b/go.sum @@ -78,6 +78,8 @@ github.com/census-instrumentation/opencensus-proto v0.2.0 h1:LzQXZOgg4CQfE6bFvXG github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cheekybits/genny v0.0.0-20170328200008-9127e812e1e9/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coredns/federation v0.0.0-20190818181423-e032b096babe h1:ND08lR/TclI9W4dScCwdRESOacCCdF3FkuB5pBIOv1U= +github.com/coredns/federation v0.0.0-20190818181423-e032b096babe/go.mod h1:MoqTEFX8GlnKkyq8eBCF94VzkNAOgjdlCJ+Pz/oCLPk= github.com/coreos/bbolt v1.3.2 h1:wZwiHHUieZCquLkDL0B8UhzreNWsPHooDAG3q34zk0s= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.13+incompatible h1:8F3hqu9fGYLBifCmRCJsicFqDx/D68Rt3q1JMazcgBQ= diff --git a/plugin.cfg b/plugin.cfg index be9447534..2bd07c557 100644 --- a/plugin.cfg +++ b/plugin.cfg @@ -47,7 +47,7 @@ hosts:hosts route53:route53 azure:azure clouddns:clouddns -federation:federation +federation:github.com/coredns/federation k8s_external:k8s_external kubernetes:kubernetes file:file diff --git a/plugin/federation/OWNERS b/plugin/federation/OWNERS deleted file mode 100644 index 187c629c9..000000000 --- a/plugin/federation/OWNERS +++ /dev/null @@ -1,6 +0,0 @@ -reviewers: - - chrisohaver - - miekg -approvers: - - chrisohaver - - miekg diff --git a/plugin/federation/README.md b/plugin/federation/README.md deleted file mode 100644 index 96ba213ed..000000000 --- a/plugin/federation/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# federation - -## Name - -*federation* - enables federated queries to be resolved via the kubernetes plugin. - -## Description - -Enabling this plugin allows -[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. - -~~~ -. { - kubernetes cluster.local - federation cluster.local { - prod prod.feddomain.com - staging staging.feddomain.com - } - forward . 192.168.1.12 -} -~~~ diff --git a/plugin/federation/federation.go b/plugin/federation/federation.go deleted file mode 100644 index 7fe2a6f62..000000000 --- a/plugin/federation/federation.go +++ /dev/null @@ -1,147 +0,0 @@ -/* -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 ( - "context" - - "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/plugin/pkg/upstream" - "github.com/coredns/coredns/request" - - "github.com/miekg/dns" -) - -// Federation contains the name to zone mapping used for federation in kubernetes. -type Federation struct { - f map[string]string - zones []string - Upstream *upstream.Upstream - - 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 - } - - 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 = true - - m.Answer = []dns.RR{service.NewCNAME(state.QName(), service.Host)} - - if f.Upstream != nil { - aRecord, err := f.Upstream.Lookup(ctx, state, service.Host, state.QType()) - if err == nil && aRecord != nil && len(aRecord.Answer) > 0 { - m.Answer = append(m.Answer, aRecord.Answer...) - } - } - - 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 deleted file mode 100644 index 64d6272b7..000000000 --- a/plugin/federation/federation_test.go +++ /dev/null @@ -1,119 +0,0 @@ -package federation - -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 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{zone: "fd-az", region: "fd-r"} - - 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 := dnstest.NewRecorder(&test.ResponseWriter{}) - _, err := fed.ServeDNS(ctx, rec, m) - if err != nil { - t.Errorf("Test %d, expected no error, got %v", i, err) - return - } - - resp := rec.Msg - if err := test.SortAndCheck(resp, tc); err != nil { - t.Error(err) - } - } -} - -func TestFederationKubernetesMissingLabels(t *testing.T) { - tests := []test.Case{ - { - // 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{zone: "", region: ""} - - 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 _, tc := range tests { - m := tc.Msg() - - rec := dnstest.NewRecorder(&test.ResponseWriter{}) - _, err := fed.ServeDNS(ctx, rec, m) - if err == nil { - t.Errorf("Expected an error") - return - } - } -} diff --git a/plugin/federation/kubernetes_api_test.go b/plugin/federation/kubernetes_api_test.go deleted file mode 100644 index 35b058b07..000000000 --- a/plugin/federation/kubernetes_api_test.go +++ /dev/null @@ -1,140 +0,0 @@ -package federation - -import ( - "github.com/coredns/coredns/plugin/kubernetes" - "github.com/coredns/coredns/plugin/kubernetes/object" - - api "k8s.io/api/core/v1" - meta "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -type APIConnFederationTest struct { - zone, region string -} - -func (APIConnFederationTest) HasSynced() bool { return true } -func (APIConnFederationTest) Run() { return } -func (APIConnFederationTest) Stop() error { return nil } -func (APIConnFederationTest) SvcIndexReverse(string) []*object.Service { return nil } -func (APIConnFederationTest) EpIndexReverse(string) []*object.Endpoints { return nil } -func (APIConnFederationTest) Modified() int64 { return 0 } - -func (APIConnFederationTest) PodIndex(string) []*object.Pod { - return []*object.Pod{ - {Namespace: "podns", PodIP: "10.240.0.1"}, // Remote IP set in test.ResponseWriter - } -} - -func (APIConnFederationTest) SvcIndex(string) []*object.Service { - svcs := []*object.Service{ - { - Name: "svc1", - Namespace: "testns", - ClusterIP: "10.0.0.1", - Ports: []api.ServicePort{ - {Name: "http", Protocol: "tcp", Port: 80}, - }, - }, - { - Name: "hdls1", - Namespace: "testns", - ClusterIP: api.ClusterIPNone, - }, - { - Name: "external", - Namespace: "testns", - ExternalName: "ext.interwebs.test", - Ports: []api.ServicePort{ - {Name: "http", Protocol: "tcp", Port: 80}, - }, - }, - } - return svcs -} - -func (APIConnFederationTest) ServiceList() []*object.Service { - svcs := []*object.Service{ - { - Name: "svc1", - Namespace: "testns", - ClusterIP: "10.0.0.1", - Ports: []api.ServicePort{ - {Name: "http", Protocol: "tcp", Port: 80}, - }, - }, - { - Name: "hdls1", - Namespace: "testns", - ClusterIP: api.ClusterIPNone, - }, - { - Name: "external", - Namespace: "testns", - ExternalName: "ext.interwebs.test", - Ports: []api.ServicePort{ - {Name: "http", Protocol: "tcp", Port: 80}, - }, - }, - } - return svcs -} - -func (APIConnFederationTest) EpIndex(string) []*object.Endpoints { - eps := []*object.Endpoints{ - { - Subsets: []object.EndpointSubset{ - { - Addresses: []object.EndpointAddress{ - {IP: "172.0.0.1", Hostname: "ep1a"}, - }, - Ports: []object.EndpointPort{ - {Port: 80, Protocol: "tcp", Name: "http"}, - }, - }, - }, - Name: "svc1", - Namespace: "testns", - }, - } - return eps -} - -func (APIConnFederationTest) EndpointsList() []*object.Endpoints { - eps := []*object.Endpoints{ - { - Subsets: []object.EndpointSubset{ - { - Addresses: []object.EndpointAddress{ - {IP: "172.0.0.1", Hostname: "ep1a"}, - }, - Ports: []object.EndpointPort{ - {Port: 80, Protocol: "tcp", Name: "http"}, - }, - }, - }, - Name: "svc1", - Namespace: "testns", - }, - } - return eps -} - -func (a APIConnFederationTest) GetNodeByName(name string) (*api.Node, error) { - return &api.Node{ - ObjectMeta: meta.ObjectMeta{ - Name: "test.node.foo.bar", - Labels: map[string]string{ - kubernetes.LabelRegion: a.region, - kubernetes.LabelZone: a.zone, - }, - }, - }, nil -} - -func (APIConnFederationTest) GetNamespaceByName(name string) (*api.Namespace, error) { - return &api.Namespace{ - ObjectMeta: meta.ObjectMeta{ - Name: name, - }, - }, nil -} diff --git a/plugin/federation/log_test.go b/plugin/federation/log_test.go deleted file mode 100644 index fa9481726..000000000 --- a/plugin/federation/log_test.go +++ /dev/null @@ -1,5 +0,0 @@ -package federation - -import clog "github.com/coredns/coredns/plugin/pkg/log" - -func init() { clog.Discard() } diff --git a/plugin/federation/setup.go b/plugin/federation/setup.go deleted file mode 100644 index fde50853d..000000000 --- a/plugin/federation/setup.go +++ /dev/null @@ -1,94 +0,0 @@ -package federation - -import ( - "fmt" - - "github.com/coredns/coredns/core/dnsserver" - "github.com/coredns/coredns/plugin" - "github.com/coredns/coredns/plugin/kubernetes" - "github.com/coredns/coredns/plugin/pkg/upstream" - "github.com/miekg/dns" - - "github.com/caddyserver/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() - fed.Upstream = upstream.New() - - for c.Next() { - // federation [zones..] - zones := c.RemainingArgs() - var 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 { - case "upstream": - // remove soon - c.RemainingArgs() - 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 deleted file mode 100644 index 6aed5cce7..000000000 --- a/plugin/federation/setup_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package federation - -import ( - "testing" - - "github.com/caddyserver/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) - } - } - } -} diff --git a/plugin/kubernetes/federation.go b/plugin/kubernetes/federation.go deleted file mode 100644 index bf169b911..000000000 --- a/plugin/kubernetes/federation.go +++ /dev/null @@ -1,51 +0,0 @@ -package kubernetes - -import ( - "errors" - - "github.com/coredns/coredns/plugin/etcd/msg" - "github.com/coredns/coredns/plugin/pkg/dnsutil" - "github.com/coredns/coredns/request" -) - -// The federation node.Labels keys used. -const ( - // TODO: Do not hardcode these labels. Pull them out of the API instead. - // - // We can get them via .... - // import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - // metav1.LabelZoneFailureDomain - // metav1.LabelZoneRegion - // - // But importing above breaks coredns with flag collision of 'log_dir' - - LabelZone = "failure-domain.beta.kubernetes.io/zone" - LabelRegion = "failure-domain.beta.kubernetes.io/region" -) - -// Federations is used from the federations plugin to return the service that should be -// returned as a CNAME for federation(s) to work. -func (k *Kubernetes) Federations(state request.Request, fname, fzone string) (msg.Service, error) { - nodeName := k.localNodeName() - node, err := k.APIConn.GetNodeByName(nodeName) - if err != nil { - return msg.Service{}, err - } - r, err := parseRequest(state) - if err != nil { - return msg.Service{}, err - } - - lz := node.Labels[LabelZone] - lr := node.Labels[LabelRegion] - - if lz == "" || lr == "" { - return msg.Service{}, errors.New("local node missing zone/region labels") - } - - if r.endpoint == "" { - return msg.Service{Host: dnsutil.Join(r.service, r.namespace, fname, r.podOrSvc, lz, lr, fzone)}, nil - } - - return msg.Service{Host: dnsutil.Join(r.endpoint, r.service, r.namespace, fname, r.podOrSvc, lz, lr, fzone)}, nil -} diff --git a/plugin/kubernetes/local.go b/plugin/kubernetes/local.go index e15fec497..c5918a306 100644 --- a/plugin/kubernetes/local.go +++ b/plugin/kubernetes/local.go @@ -21,7 +21,8 @@ func localPodIP() net.IP { return nil } -func (k *Kubernetes) localNodeName() string { +// LocalNodeName is exclusively used in federation plugin, will be deprecated later. +func (k *Kubernetes) LocalNodeName() string { localIP := k.interfaceAddrsFunc() if localIP == nil { return ""