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:
parent
cc4e4a0626
commit
f96cf27193
25 changed files with 727 additions and 123 deletions
|
@ -30,6 +30,7 @@ var directives = []string{
|
|||
"dnssec",
|
||||
"reverse",
|
||||
"hosts",
|
||||
"federation",
|
||||
"kubernetes",
|
||||
"file",
|
||||
"auto",
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
_ "github.com/coredns/coredns/middleware/erratic"
|
||||
_ "github.com/coredns/coredns/middleware/errors"
|
||||
_ "github.com/coredns/coredns/middleware/etcd"
|
||||
_ "github.com/coredns/coredns/middleware/federation"
|
||||
_ "github.com/coredns/coredns/middleware/file"
|
||||
_ "github.com/coredns/coredns/middleware/health"
|
||||
_ "github.com/coredns/coredns/middleware/hosts"
|
||||
|
|
|
@ -38,13 +38,14 @@
|
|||
160:dnssec:dnssec
|
||||
170:reverse:reverse
|
||||
180:hosts:hosts
|
||||
190:kubernetes:kubernetes
|
||||
200:file:file
|
||||
210:auto:auto
|
||||
220:secondary:secondary
|
||||
230:etcd:etcd
|
||||
240:proxy:proxy
|
||||
250:erratic:erratic
|
||||
260:whoami:whoami
|
||||
190:federation:federation
|
||||
200:kubernetes:kubernetes
|
||||
210:file:file
|
||||
220:auto:auto
|
||||
230:secondary:secondary
|
||||
240:etcd:etcd
|
||||
250:proxy:proxy
|
||||
260:erratic:erratic
|
||||
270:whoami:whoami
|
||||
500:startup:github.com/mholt/caddy/startupshutdown
|
||||
510:shutdown:github.com/mholt/caddy/startupshutdown
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
package autopath
|
||||
|
||||
/*
|
||||
Autopath is a hack; it shortcuts the client's search path resolution by performing
|
||||
these lookups on the server...
|
||||
Autopath package implement autopathing. This is a hack; it shortcuts the
|
||||
client's search path resolution by performing these lookups on the server...
|
||||
|
||||
The server has a copy (via AutoPathFunc) of the client's search path and on
|
||||
receiving a query it first establish if the suffix matches the FIRST configured
|
||||
|
@ -31,6 +29,7 @@ func (m Middleware ) AutoPath(state request.Request) []string {
|
|||
return []string{"first", "second", "last", ""}
|
||||
}
|
||||
*/
|
||||
package autopath
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
@ -108,7 +107,6 @@ func (a *AutoPath) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Ms
|
|||
if err != nil {
|
||||
// Return now - not sure if this is the best. We should also check if the write has happened.
|
||||
return rcode, err
|
||||
|
||||
}
|
||||
if i == 0 {
|
||||
firstReply = nw.Msg
|
||||
|
|
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
43
middleware/kubernetes/federation.go
Normal file
43
middleware/kubernetes/federation.go
Normal file
|
@ -0,0 +1,43 @@
|
|||
package kubernetes
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/coredns/coredns/middleware/etcd/msg"
|
||||
"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 middleware 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 := k.parseRequest(state)
|
||||
|
||||
lz := node.Labels[LabelZone]
|
||||
lr := node.Labels[LabelRegion]
|
||||
|
||||
if r.endpoint == "" {
|
||||
return msg.Service{Host: strings.Join([]string{r.service, r.namespace, fname, r.podOrSvc, lz, lr, fzone}, ".")}, nil
|
||||
}
|
||||
|
||||
return msg.Service{Host: strings.Join([]string{r.endpoint, r.service, r.namespace, fname, r.podOrSvc, lz, lr, fzone}, ".")}, nil
|
||||
}
|
|
@ -146,10 +146,8 @@ var podModeVerifiedCases = map[string](test.Case){
|
|||
|
||||
func TestServeDNS(t *testing.T) {
|
||||
|
||||
k := Kubernetes{Zones: []string{"cluster.local."}}
|
||||
|
||||
k := New([]string{"cluster.local."})
|
||||
k.APIConn = &APIConnServeTest{}
|
||||
k.interfaceAddrsFunc = localPodIP
|
||||
k.Next = test.NextHandler(dns.RcodeSuccess, nil)
|
||||
|
||||
ctx := context.TODO()
|
||||
|
@ -166,7 +164,7 @@ func TestServeDNS(t *testing.T) {
|
|||
runServeDNSTests(ctx, t, podModeVerifiedCases, k)
|
||||
}
|
||||
|
||||
func runServeDNSTests(ctx context.Context, t *testing.T, dnsTestCases map[string](test.Case), k Kubernetes) {
|
||||
func runServeDNSTests(ctx context.Context, t *testing.T, dnsTestCases map[string](test.Case), k *Kubernetes) {
|
||||
for testname, tc := range dnsTestCases {
|
||||
r := tc.Msg()
|
||||
|
||||
|
|
|
@ -51,6 +51,16 @@ type Kubernetes struct {
|
|||
autoPathSearch []string // Local search path from /etc/resolv.conf. Needed for autopath.
|
||||
}
|
||||
|
||||
// New returns a intialized Kubernetes. It default interfaceAddrFunc to return 127.0.0.1. All other
|
||||
// values default to their zero value, primaryZoneIndex will thus point to the first zone.
|
||||
func New(zones []string) *Kubernetes {
|
||||
k := new(Kubernetes)
|
||||
k.Zones = zones
|
||||
k.interfaceAddrsFunc = func() net.IP { return net.ParseIP("127.0.0.1") }
|
||||
|
||||
return k
|
||||
}
|
||||
|
||||
const (
|
||||
// PodModeDisabled is the default value where pod requests are ignored
|
||||
PodModeDisabled = "disabled"
|
||||
|
@ -96,21 +106,21 @@ func (k *Kubernetes) Services(state request.Request, exact bool, opt middleware.
|
|||
|
||||
// We're looking again at types, which we've already done in ServeDNS, but there are some types k8s just can't answer.
|
||||
switch state.QType() {
|
||||
|
||||
case dns.TypeTXT:
|
||||
// 1 label + zone, label must be "dns-version".
|
||||
t, err := dnsutil.TrimZone(state.Name(), state.Zone)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
t, _ := dnsutil.TrimZone(state.Name(), state.Zone)
|
||||
|
||||
segs := dns.SplitDomainName(t)
|
||||
if len(segs) != 1 {
|
||||
return nil, nil, errors.New("servfail")
|
||||
return nil, nil, fmt.Errorf("kubernetes: TXT query can only be for dns-version: %s", state.QName())
|
||||
}
|
||||
if segs[0] != "dns-version" {
|
||||
return nil, nil, errInvalidRequest
|
||||
return nil, nil, nil
|
||||
}
|
||||
svc := msg.Service{Text: DNSSchemaVersion, TTL: 28800, Key: msg.Path(state.QName(), "coredns")}
|
||||
return []msg.Service{svc}, nil, nil
|
||||
|
||||
case dns.TypeNS:
|
||||
// We can only get here if the qname equal the zone, see ServeDNS in handler.go.
|
||||
ns := k.nsAddr()
|
||||
|
@ -118,38 +128,30 @@ func (k *Kubernetes) Services(state request.Request, exact bool, opt middleware.
|
|||
return []msg.Service{svc}, nil, nil
|
||||
}
|
||||
|
||||
r, e := k.parseRequest(state)
|
||||
if e != nil {
|
||||
return nil, nil, e
|
||||
}
|
||||
|
||||
switch state.QType() {
|
||||
case dns.TypeA, dns.TypeAAAA, dns.TypeCNAME:
|
||||
if state.Type() == "A" && isDefaultNS(state.Name(), r) {
|
||||
if state.QType() == dns.TypeA && isDefaultNS(state.Name(), state.Zone) {
|
||||
// If this is an A request for "ns.dns", respond with a "fake" record for coredns.
|
||||
// SOA records always use this hardcoded name
|
||||
ns := k.nsAddr()
|
||||
svc := msg.Service{Host: ns.A.String(), Key: msg.Path(state.QName(), "coredns")}
|
||||
return []msg.Service{svc}, nil, nil
|
||||
}
|
||||
s, e := k.Entries(r)
|
||||
if state.QType() == dns.TypeAAAA {
|
||||
// AAAA not implemented
|
||||
return nil, nil, e
|
||||
|
||||
s, e := k.Entries(state)
|
||||
|
||||
// SRV for external services is not yet implemented, so remove those records.
|
||||
|
||||
if state.QType() != dns.TypeSRV {
|
||||
return s, nil, e
|
||||
}
|
||||
return s, nil, e // Haven't implemented debug queries yet.
|
||||
case dns.TypeSRV:
|
||||
s, e := k.Entries(r)
|
||||
// SRV for external services is not yet implemented, so remove those records
|
||||
noext := []msg.Service{}
|
||||
|
||||
internal := []msg.Service{}
|
||||
for _, svc := range s {
|
||||
if t, _ := svc.HostType(); t != dns.TypeCNAME {
|
||||
noext = append(noext, svc)
|
||||
internal = append(internal, svc)
|
||||
}
|
||||
}
|
||||
return noext, nil, e
|
||||
}
|
||||
return nil, nil, nil
|
||||
|
||||
return internal, nil, e
|
||||
}
|
||||
|
||||
// primaryZone will return the first non-reverse zone being handled by this middleware
|
||||
|
@ -247,9 +249,11 @@ func (k *Kubernetes) getClientConfig() (*rest.Config, error) {
|
|||
if len(k.APIClientKey) > 0 {
|
||||
authinfo.ClientKey = k.APIClientKey
|
||||
}
|
||||
|
||||
overrides.ClusterInfo = clusterinfo
|
||||
overrides.AuthInfo = authinfo
|
||||
clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, overrides)
|
||||
|
||||
return clientConfig.ClientConfig()
|
||||
}
|
||||
|
||||
|
@ -263,7 +267,7 @@ func (k *Kubernetes) InitKubeCache() (err error) {
|
|||
|
||||
kubeClient, err := kubernetes.NewForConfig(config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create kubernetes notification controller: %v", err)
|
||||
return fmt.Errorf("failed to create kubernetes notification controller: %q", err)
|
||||
}
|
||||
|
||||
if k.LabelSelector != nil {
|
||||
|
@ -271,12 +275,12 @@ func (k *Kubernetes) InitKubeCache() (err error) {
|
|||
selector, err = unversionedapi.LabelSelectorAsSelector(k.LabelSelector)
|
||||
k.Selector = &selector
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create Selector for LabelSelector '%s'.Error was: %s", k.LabelSelector, err)
|
||||
return fmt.Errorf("unable to create Selector for LabelSelector '%s': %q", k.LabelSelector, err)
|
||||
}
|
||||
}
|
||||
|
||||
if k.LabelSelector != nil {
|
||||
log.Printf("[INFO] Kubernetes middleware configured with the label selector '%s'. Only kubernetes objects matching this label selector will be exposed.", unversionedapi.FormatLabelSelector(k.LabelSelector))
|
||||
log.Printf("[INFO] Kubernetes has label selector '%s'. Only objects matching this label selector will be exposed.", unversionedapi.FormatLabelSelector(k.LabelSelector))
|
||||
}
|
||||
|
||||
opts := dnsControlOpts{
|
||||
|
@ -287,20 +291,22 @@ func (k *Kubernetes) InitKubeCache() (err error) {
|
|||
return err
|
||||
}
|
||||
|
||||
// Records not implemented, see Entries().
|
||||
// Records is not implemented.
|
||||
func (k *Kubernetes) Records(name string, exact bool) ([]msg.Service, error) {
|
||||
return nil, fmt.Errorf("NOOP")
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
// Entries looks up services in kubernetes. If exact is true, it will lookup
|
||||
// just this name. This is used when find matches when completing SRV lookups
|
||||
// for instance.
|
||||
func (k *Kubernetes) Entries(r recordRequest) ([]msg.Service, error) {
|
||||
// Abort if the namespace does not contain a wildcard, and namespace is not published per CoreFile
|
||||
// Case where namespace contains a wildcard is handled in Get(...) method.
|
||||
if (!wildcard(r.namespace)) && (len(k.Namespaces) > 0) && (!dnsstrings.StringInSlice(r.namespace, k.Namespaces)) {
|
||||
// Entries looks up services in kubernetes.
|
||||
func (k *Kubernetes) Entries(state request.Request) ([]msg.Service, error) {
|
||||
r, e := k.parseRequest(state)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
|
||||
if !k.namespaceExposed(r.namespace) {
|
||||
return nil, errNsNotExposed
|
||||
}
|
||||
|
||||
services, pods, err := k.get(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -310,7 +316,6 @@ func (k *Kubernetes) Entries(r recordRequest) ([]msg.Service, error) {
|
|||
}
|
||||
|
||||
records := k.getRecordsForK8sItems(services, pods, r)
|
||||
|
||||
return records, nil
|
||||
}
|
||||
|
||||
|
@ -432,6 +437,7 @@ func (k *Kubernetes) findServices(r recordRequest) ([]kService, error) {
|
|||
if !(match(r.namespace, svc.Namespace, nsWildcard) && match(r.service, svc.Name, serviceWildcard)) {
|
||||
continue
|
||||
}
|
||||
|
||||
// If namespace has a wildcard, filter results against Corefile namespace list.
|
||||
// (Namespaces without a wildcard were filtered before the call to this function.)
|
||||
if nsWildcard && (len(k.Namespaces) > 0) && (!dnsstrings.StringInSlice(svc.Namespace, k.Namespaces)) {
|
||||
|
@ -529,28 +535,22 @@ func (k *Kubernetes) getServiceRecordForIP(ip, name string) []msg.Service {
|
|||
return nil
|
||||
}
|
||||
|
||||
// namespaceExposed returns true when the namespace is exposed.
|
||||
func (k *Kubernetes) namespaceExposed(namespace string) bool {
|
||||
// Abort if the namespace does not contain a wildcard, and namespace is
|
||||
// not published per CoreFile Case where namespace contains a wildcard
|
||||
// is handled in k.get(...) method.
|
||||
if (!wildcard(namespace)) && (len(k.Namespaces) > 0) && (!dnsstrings.StringInSlice(namespace, k.Namespaces)) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// wildcard checks whether s contains a wildcard value
|
||||
func wildcard(s string) bool {
|
||||
return (s == "*" || s == "any")
|
||||
}
|
||||
|
||||
func localPodIP() net.IP {
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, addr := range addrs {
|
||||
ip, _, _ := net.ParseCIDR(addr.String())
|
||||
ip = ip.To4()
|
||||
if ip == nil || ip.IsLoopback() {
|
||||
continue
|
||||
}
|
||||
return ip
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
// Svc is the DNS schema for kubernetes services
|
||||
Svc = "svc"
|
||||
|
|
|
@ -195,8 +195,7 @@ func (APIConnServiceTest) GetNodeByName(name string) (api.Node, error) {
|
|||
|
||||
func TestServices(t *testing.T) {
|
||||
|
||||
k := Kubernetes{Zones: []string{"interwebs.test."}}
|
||||
k.interfaceAddrsFunc = localPodIP
|
||||
k := New([]string{"interwebs.test."})
|
||||
k.APIConn = &APIConnServiceTest{}
|
||||
|
||||
type svcAns struct {
|
||||
|
|
40
middleware/kubernetes/local.go
Normal file
40
middleware/kubernetes/local.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
package kubernetes
|
||||
|
||||
import "net"
|
||||
|
||||
func localPodIP() net.IP {
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, addr := range addrs {
|
||||
ip, _, _ := net.ParseCIDR(addr.String())
|
||||
ip = ip.To4()
|
||||
if ip == nil || ip.IsLoopback() {
|
||||
continue
|
||||
}
|
||||
return ip
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *Kubernetes) localNodeName() string {
|
||||
localIP := k.interfaceAddrsFunc()
|
||||
if localIP == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Find endpoint matching localIP
|
||||
endpointsList := k.APIConn.EndpointsList()
|
||||
for _, ep := range endpointsList.Items {
|
||||
for _, eps := range ep.Subsets {
|
||||
for _, addr := range eps.Addresses {
|
||||
if localIP.Equal(net.ParseIP(addr.IP)) {
|
||||
return *addr.NodeName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
|
@ -8,8 +8,8 @@ import (
|
|||
"k8s.io/client-go/1.5/pkg/api"
|
||||
)
|
||||
|
||||
func isDefaultNS(name string, r recordRequest) bool {
|
||||
return strings.Index(name, defaultNSName) == 0 && strings.Index(name, r.zone) == len(defaultNSName)
|
||||
func isDefaultNS(name, zone string) bool {
|
||||
return strings.Index(name, defaultNSName) == 0 && strings.Index(name, zone) == len(defaultNSName)
|
||||
}
|
||||
|
||||
func (k *Kubernetes) nsAddr() *dns.A {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package kubernetes
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"k8s.io/client-go/1.5/pkg/api"
|
||||
|
@ -36,7 +35,7 @@ func (APIConnTest) EndpointsList() api.EndpointsList {
|
|||
{
|
||||
Addresses: []api.EndpointAddress{
|
||||
{
|
||||
IP: "172.0.40.10",
|
||||
IP: "127.0.0.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -54,8 +53,7 @@ func (APIConnTest) GetNodeByName(name string) (api.Node, error) { return api.Nod
|
|||
|
||||
func TestNsAddr(t *testing.T) {
|
||||
|
||||
k := Kubernetes{Zones: []string{"inter.webs.test"}}
|
||||
k.interfaceAddrsFunc = func() net.IP { return net.ParseIP("172.0.40.10") }
|
||||
k := New([]string{"inter.webs.test."})
|
||||
k.APIConn = &APIConnTest{}
|
||||
|
||||
cdr := k.nsAddr()
|
||||
|
|
|
@ -25,24 +25,17 @@ type recordRequest struct {
|
|||
|
||||
// parseRequest parses the qname to find all the elements we need for querying k8s.
|
||||
func (k *Kubernetes) parseRequest(state request.Request) (r recordRequest, err error) {
|
||||
// 3 Possible cases: TODO(chris): remove federations comments here.
|
||||
// SRV Request: _port._protocol.service.namespace.[federation.]type.zone
|
||||
// A Request (endpoint): endpoint.service.namespace.[federation.]type.zone
|
||||
// A Request (service): service.namespace.[federation.]type.zone
|
||||
// 3 Possible cases:
|
||||
// o SRV Request: _port._protocol.service.namespace.type.zone
|
||||
// o A Request (endpoint): endpoint.service.namespace.type.zone
|
||||
// o A Request (service): service.namespace.type.zone
|
||||
// Federations are handled in the federation middleware.
|
||||
|
||||
base, _ := dnsutil.TrimZone(state.Name(), state.Zone)
|
||||
segs := dns.SplitDomainName(base)
|
||||
|
||||
r.zone = state.Zone
|
||||
|
||||
if state.QType() == dns.TypeNS {
|
||||
return r, nil
|
||||
}
|
||||
|
||||
if state.QType() == dns.TypeA && isDefaultNS(state.Name(), r) {
|
||||
return r, nil
|
||||
}
|
||||
|
||||
offset := 0
|
||||
if state.QType() == dns.TypeSRV {
|
||||
// The kubernetes peer-finder expects queries with empty port and service to resolve
|
||||
|
@ -99,8 +92,7 @@ func (k *Kubernetes) parseRequest(state request.Request) (r recordRequest, err e
|
|||
return r, errInvalidRequest
|
||||
}
|
||||
|
||||
// String return a string representation of r, it just returns all
|
||||
// fields concatenated with dots.
|
||||
// String return a string representation of r, it just returns all fields concatenated with dots.
|
||||
// This is mostly used in tests.
|
||||
func (r recordRequest) String() string {
|
||||
s := r.port
|
||||
|
|
|
@ -4,11 +4,12 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/coredns/coredns/request"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func TestParseRequest(t *testing.T) {
|
||||
k := Kubernetes{Zones: []string{zone}}
|
||||
k := New([]string{zone})
|
||||
|
||||
tests := []struct {
|
||||
query string
|
||||
|
@ -30,10 +31,6 @@ func TestParseRequest(t *testing.T) {
|
|||
"1-2-3-4.webs.mynamespace.svc.inter.webs.test.", dns.TypeA,
|
||||
"..1-2-3-4.webs.mynamespace.svc.intern.webs.tests.",
|
||||
},
|
||||
{
|
||||
"inter.webs.test.", dns.TypeNS,
|
||||
"......intern.webs.tests.",
|
||||
},
|
||||
}
|
||||
for i, tc := range tests {
|
||||
m := new(dns.Msg)
|
||||
|
@ -52,7 +49,7 @@ func TestParseRequest(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestParseInvalidRequest(t *testing.T) {
|
||||
k := Kubernetes{Zones: []string{zone}}
|
||||
k := New([]string{zone})
|
||||
|
||||
invalid := map[string]uint16{
|
||||
"_http._tcp.webs.mynamespace.svc.inter.webs.test.": dns.TypeA, // A requests cannot have port or protocol
|
||||
|
|
|
@ -77,8 +77,7 @@ func (APIConnReverseTest) GetNodeByName(name string) (api.Node, error) {
|
|||
|
||||
func TestReverse(t *testing.T) {
|
||||
|
||||
k := Kubernetes{Zones: []string{"cluster.local.", "0.10.in-addr.arpa."}}
|
||||
k.interfaceAddrsFunc = localPodIP
|
||||
k := New([]string{"cluster.local.", "0.10.in-addr.arpa."})
|
||||
k.APIConn = &APIConnReverseTest{}
|
||||
|
||||
tests := []test.Case{
|
||||
|
|
|
@ -67,10 +67,9 @@ func kubernetesParse(c *caddy.Controller) (*Kubernetes, error) {
|
|||
interfaceAddrsFunc: localPodIP,
|
||||
PodMode: PodModeDisabled,
|
||||
Proxy: proxy.Proxy{},
|
||||
autoPathSearch: searchFromResolvConf(),
|
||||
}
|
||||
|
||||
k8s.autoPathSearch = searchFromResolvConf()
|
||||
|
||||
for c.Next() {
|
||||
zones := c.RemainingArgs()
|
||||
|
||||
|
|
|
@ -19,12 +19,12 @@ func TestKubernetesParseReverseZone(t *testing.T) {
|
|||
c := caddy.NewTestController("dns", tc.input)
|
||||
k, err := kubernetesParse(c)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Expected no error, got %q", err)
|
||||
t.Fatalf("Test %d: Expected no error, got %q", i, err)
|
||||
}
|
||||
|
||||
zl := len(k.Zones)
|
||||
if zl != len(tc.expectedZones) {
|
||||
t.Errorf("Test %d: Expected kubernetes controller to be initialized with %d zones, found %d zones: '%v'", i, len(tc.expectedZones), zl)
|
||||
t.Errorf("Test %d: Expected kubernetes to be initialized with %d zones, found %d zones", i, len(tc.expectedZones), zl)
|
||||
}
|
||||
for i, z := range tc.expectedZones {
|
||||
if k.Zones[i] != z {
|
||||
|
|
|
@ -218,35 +218,35 @@ func Section(t *testing.T, tc Case, sec sect, rr []dns.RR) bool {
|
|||
case *dns.SOA:
|
||||
tt := section[i].(*dns.SOA)
|
||||
if x.Ns != tt.Ns {
|
||||
t.Errorf("SOA nameserver should be %q, but is %q", x.Ns, tt.Ns)
|
||||
t.Errorf("SOA nameserver should be %q, but is %q", tt.Ns, x.Ns)
|
||||
return false
|
||||
}
|
||||
case *dns.PTR:
|
||||
tt := section[i].(*dns.PTR)
|
||||
if x.Ptr != tt.Ptr {
|
||||
t.Errorf("PTR ptr should be %q, but is %q", x.Ptr, tt.Ptr)
|
||||
t.Errorf("PTR ptr should be %q, but is %q", tt.Ptr, x.Ptr)
|
||||
return false
|
||||
}
|
||||
case *dns.CNAME:
|
||||
tt := section[i].(*dns.CNAME)
|
||||
if x.Target != tt.Target {
|
||||
t.Errorf("CNAME target should be %q, but is %q", x.Target, tt.Target)
|
||||
t.Errorf("CNAME target should be %q, but is %q", tt.Target, x.Target)
|
||||
return false
|
||||
}
|
||||
case *dns.MX:
|
||||
tt := section[i].(*dns.MX)
|
||||
if x.Mx != tt.Mx {
|
||||
t.Errorf("MX Mx should be %q, but is %q", x.Mx, tt.Mx)
|
||||
t.Errorf("MX Mx should be %q, but is %q", tt.Mx, x.Mx)
|
||||
return false
|
||||
}
|
||||
if x.Preference != tt.Preference {
|
||||
t.Errorf("MX Preference should be %q, but is %q", x.Preference, tt.Preference)
|
||||
t.Errorf("MX Preference should be %q, but is %q", tt.Preference, x.Preference)
|
||||
return false
|
||||
}
|
||||
case *dns.NS:
|
||||
tt := section[i].(*dns.NS)
|
||||
if x.Ns != tt.Ns {
|
||||
t.Errorf("NS nameserver should be %q, but is %q", x.Ns, tt.Ns)
|
||||
t.Errorf("NS nameserver should be %q, but is %q", tt.Ns, x.Ns)
|
||||
return false
|
||||
}
|
||||
case *dns.OPT:
|
||||
|
|
|
@ -357,7 +357,7 @@ func doIntegrationTests(t *testing.T, corefile string, testCases []test.Case) {
|
|||
}
|
||||
|
||||
if len(res.Answer) != len(tc.Answer) {
|
||||
t.Errorf("Expected %d answers but got %d for query %s, %d", len(tc.Answer), len(res.Answer), tc.Qname, tc.Qtype)
|
||||
t.Errorf("Expected %d answers but got %d for query %s, %d\nfull reply %s", len(tc.Answer), len(res.Answer), tc.Qname, tc.Qtype, res)
|
||||
}
|
||||
|
||||
//TODO: Check the actual RR values
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue