Move federation plugin to github.com/coredns/federation (#3139)

* Remove federation

Signed-off-by: Yong Tang <yong.tang.github@outlook.com>

* Rebuild and point to github.com/coredns/federation

Signed-off-by: Yong Tang <yong.tang.github@outlook.com>

* Export `localNodeName` => `LocalNodeName`, to be used by federation (until deprecation)

Signed-off-by: Yong Tang <yong.tang.github@outlook.com>

* Remove plugin/kubernetes/federation.go (=> kubernetes/federation repo)

Signed-off-by: Yong Tang <yong.tang.github@outlook.com>

* Update github.com/coredns/federation

Signed-off-by: Yong Tang <yong.tang.github@outlook.com>

* sticker-ci fix

Signed-off-by: Yong Tang <yong.tang.github@outlook.com>
This commit is contained in:
Yong Tang 2019-08-18 14:41:51 -07:00 committed by GitHub
parent bbf148360b
commit 6402cef337
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 7 additions and 669 deletions

View file

@ -20,7 +20,6 @@ import (
_ "github.com/coredns/coredns/plugin/erratic" _ "github.com/coredns/coredns/plugin/erratic"
_ "github.com/coredns/coredns/plugin/errors" _ "github.com/coredns/coredns/plugin/errors"
_ "github.com/coredns/coredns/plugin/etcd" _ "github.com/coredns/coredns/plugin/etcd"
_ "github.com/coredns/coredns/plugin/federation"
_ "github.com/coredns/coredns/plugin/file" _ "github.com/coredns/coredns/plugin/file"
_ "github.com/coredns/coredns/plugin/forward" _ "github.com/coredns/coredns/plugin/forward"
_ "github.com/coredns/coredns/plugin/grpc" _ "github.com/coredns/coredns/plugin/grpc"
@ -45,4 +44,5 @@ import (
_ "github.com/coredns/coredns/plugin/tls" _ "github.com/coredns/coredns/plugin/tls"
_ "github.com/coredns/coredns/plugin/trace" _ "github.com/coredns/coredns/plugin/trace"
_ "github.com/coredns/coredns/plugin/whoami" _ "github.com/coredns/coredns/plugin/whoami"
_ "github.com/coredns/federation"
) )

1
go.mod
View file

@ -12,6 +12,7 @@ require (
github.com/Shopify/sarama v1.21.0 // indirect github.com/Shopify/sarama v1.21.0 // indirect
github.com/aws/aws-sdk-go v1.22.3 github.com/aws/aws-sdk-go v1.22.3
github.com/caddyserver/caddy v1.0.1 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/bbolt v1.3.2 // indirect
github.com/coreos/etcd v3.3.13+incompatible github.com/coreos/etcd v3.3.13+incompatible
github.com/coreos/go-semver v0.2.0 // indirect github.com/coreos/go-semver v0.2.0 // indirect

2
go.sum
View file

@ -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/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/cheekybits/genny v0.0.0-20170328200008-9127e812e1e9/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 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 h1:wZwiHHUieZCquLkDL0B8UhzreNWsPHooDAG3q34zk0s=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.13+incompatible h1:8F3hqu9fGYLBifCmRCJsicFqDx/D68Rt3q1JMazcgBQ= github.com/coreos/etcd v3.3.13+incompatible h1:8F3hqu9fGYLBifCmRCJsicFqDx/D68Rt3q1JMazcgBQ=

View file

@ -47,7 +47,7 @@ hosts:hosts
route53:route53 route53:route53
azure:azure azure:azure
clouddns:clouddns clouddns:clouddns
federation:federation federation:github.com/coredns/federation
k8s_external:k8s_external k8s_external:k8s_external
kubernetes:kubernetes kubernetes:kubernetes
file:file file:file

View file

@ -1,6 +0,0 @@
reviewers:
- chrisohaver
- miekg
approvers:
- chrisohaver
- miekg

View file

@ -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
}
~~~

View file

@ -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 "", ""
}

View file

@ -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
}
}
}

View file

@ -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
}

View file

@ -1,5 +0,0 @@
package federation
import clog "github.com/coredns/coredns/plugin/pkg/log"
func init() { clog.Discard() }

View file

@ -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
}

View file

@ -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)
}
}
}
}

View file

@ -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
}

View file

@ -21,7 +21,8 @@ func localPodIP() net.IP {
return nil 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() localIP := k.interfaceAddrsFunc()
if localIP == nil { if localIP == nil {
return "" return ""