Remove the word middleware (#1067)

* Rename middleware to plugin

first pass; mostly used 'sed', few spots where I manually changed
text.

This still builds a coredns binary.

* fmt error

* Rename AddMiddleware to AddPlugin

* Readd AddMiddleware to remain backwards compat
This commit is contained in:
Miek Gieben
2017-09-14 09:36:06 +01:00
committed by GitHub
parent b984aa4559
commit d8714e64e4
354 changed files with 974 additions and 969 deletions

45
plugin/autopath/README.md Normal file
View File

@@ -0,0 +1,45 @@
# autopath
The *autopath* plugin allows CoreDNS to perform server side search path completion.
If it sees a query that matches the first element of the configured search path, *autopath* will
follow the chain of search path elements and returns the first reply that is not NXDOMAIN.
On any failures the original reply is returned.
Because *autopath* returns a reply for a name that wasn't the original question it will add a CNAME
that points from the original name (with the search path element in it) to the name of this answer.
## Syntax
~~~
autopath [ZONE..] RESOLV-CONF
~~~
* **ZONES** zones *autopath* should be authoritative for.
* **RESOLV-CONF** points to a `resolv.conf` like file or uses a special syntax to point to another
plugin. For instance `@kubernetes`, will call out to the kubernetes plugin (for each
query) to retrieve the search list it should use.
Currently the following set of plugin has implemented *autopath*:
* *kubernetes*
* *erratic*
## Examples
~~~
autopath my-resolv.conf
~~~
Use `my-resolv.conf` as the file to get the search path from. This file only needs so have one line:
`search domain1 domain2 ...`
~~~
autopath @kubernetes
~~~
Use the search path dynamically retrieved from the kubernetes plugin.
## Bugs
When the *cache* plugin is enabled it is possible for pods in different namespaces to get the
same answer.

152
plugin/autopath/autopath.go Normal file
View File

@@ -0,0 +1,152 @@
/*
Package autopath implements 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
element. If no match can be found the query will be forwarded up the plugin
chain without interference (iff 'fallthrough' has been set).
If the query is deemed to fall in the search path the server will perform the
queries with each element of the search path appended in sequence until a
non-NXDOMAIN answer has been found. That reply will then be returned to the
client - with some CNAME hackery to let the client accept the reply.
If all queries return NXDOMAIN we return the original as-is and let the client
continue searching. The client will go to the next element in the search path,
but we wont do any more autopathing. It means that in the failure case, you do
more work, since the server looks it up, then the client still needs to go
through the search path.
It is assume the search path ordering is identical between server and client.
Midldeware implementing autopath, must have a function called `AutoPath` of type
autopath.Func. Note the searchpath must be ending with the empty string.
I.e:
func (m Middleware ) AutoPath(state request.Request) []string {
return []string{"first", "second", "last", ""}
}
*/
package autopath
import (
"log"
"github.com/coredns/coredns/plugin"
"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"
)
// Func defines the function plugin should implement to return a search
// path to the autopath plugin. The last element of the slice must be the empty string.
// If Func returns a nil slice, no autopathing will be done.
type Func func(request.Request) []string
// AutoPath perform autopath: service side search path completion.
type AutoPath struct {
Next plugin.Handler
Zones []string
// Search always includes "" as the last element, so we try the base query with out any search paths added as well.
search []string
searchFunc Func
}
// ServeDNS implements the plugin.Handle interface.
func (a *AutoPath) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
state := request.Request{W: w, Req: r}
zone := plugin.Zones(a.Zones).Matches(state.Name())
if zone == "" {
return plugin.NextOrFailure(a.Name(), a.Next, ctx, w, r)
}
// Check if autopath should be done, searchFunc takes precedence over the local configured search path.
var err error
searchpath := a.search
if a.searchFunc != nil {
searchpath = a.searchFunc(state)
}
if len(searchpath) == 0 {
log.Printf("[WARNING] No search path available for autopath")
return plugin.NextOrFailure(a.Name(), a.Next, ctx, w, r)
}
if !firstInSearchPath(state.Name(), searchpath) {
return plugin.NextOrFailure(a.Name(), a.Next, ctx, w, r)
}
origQName := state.QName()
// Establish base name of the query. I.e what was originally asked.
base, err := dnsutil.TrimZone(state.QName(), searchpath[0]) // TODO(miek): we loose the original case of the query here.
if err != nil {
return dns.RcodeServerFailure, err
}
firstReply := new(dns.Msg)
firstRcode := 0
var firstErr error
ar := r.Copy()
// Walk the search path and see if we can get a non-nxdomain - if they all fail we return the first
// query we've done and return that as-is. This means the client will do the search path walk again...
for i, s := range searchpath {
newQName := base + "." + s
ar.Question[0].Name = newQName
nw := nonwriter.New(w)
rcode, err := plugin.NextOrFailure(a.Name(), a.Next, ctx, nw, ar)
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
firstRcode = rcode
firstErr = err
}
if !plugin.ClientWrite(rcode) {
continue
}
if nw.Msg.Rcode == dns.RcodeNameError {
continue
}
msg := nw.Msg
cnamer(msg, origQName)
// Write whatever non-nxdomain answer we've found.
w.WriteMsg(msg)
return rcode, err
}
if plugin.ClientWrite(firstRcode) {
w.WriteMsg(firstReply)
}
return firstRcode, firstErr
}
// Name implements the Handler interface.
func (a *AutoPath) Name() string { return "autopath" }
// firstInSearchPath checks if name is equal to are a sibling of the first element in the search path.
func firstInSearchPath(name string, searchpath []string) bool {
if name == searchpath[0] {
return true
}
if dns.IsSubDomain(searchpath[0], name) {
return true
}
return false
}

View File

@@ -0,0 +1,166 @@
package autopath
import (
"testing"
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/pkg/dnsrecorder"
"github.com/coredns/coredns/plugin/test"
"github.com/miekg/dns"
"golang.org/x/net/context"
)
var autopathTestCases = []test.Case{
{
// search path expansion.
Qname: "b.example.org.", Qtype: dns.TypeA,
Answer: []dns.RR{
test.CNAME("b.example.org. 3600 IN CNAME b.com."),
test.A("b.com." + defaultA),
},
},
{
// No search path expansion
Qname: "a.example.com.", Qtype: dns.TypeA,
Answer: []dns.RR{
test.A("a.example.com." + defaultA),
},
},
}
func newTestAutoPath() *AutoPath {
ap := new(AutoPath)
ap.Zones = []string{"."}
ap.Next = nextHandler(map[string]int{
"b.example.org.": dns.RcodeNameError,
"b.com.": dns.RcodeSuccess,
"a.example.com.": dns.RcodeSuccess,
})
ap.search = []string{"example.org.", "example.com.", "com.", ""}
return ap
}
func TestAutoPath(t *testing.T) {
ap := newTestAutoPath()
ctx := context.TODO()
for _, tc := range autopathTestCases {
m := tc.Msg()
rec := dnsrecorder.New(&test.ResponseWriter{})
_, err := ap.ServeDNS(ctx, rec, m)
if err != nil {
t.Errorf("expected no error, got %v\n", err)
continue
}
// No sorting here as we want to check if the CNAME sits *before* the
// test of the answer.
resp := rec.Msg
if !test.Header(t, tc, resp) {
t.Logf("%v\n", resp)
continue
}
if !test.Section(t, tc, test.Answer, resp.Answer) {
t.Logf("%v\n", resp)
}
if !test.Section(t, tc, test.Ns, resp.Ns) {
t.Logf("%v\n", resp)
}
if !test.Section(t, tc, test.Extra, resp.Extra) {
t.Logf("%v\n", resp)
}
}
}
var autopathNoAnswerTestCases = []test.Case{
{
// search path expansion, no answer
Qname: "c.example.org.", Qtype: dns.TypeA,
Answer: []dns.RR{
test.CNAME("b.example.org. 3600 IN CNAME b.com."),
test.A("b.com." + defaultA),
},
},
}
func TestAutoPathNoAnswer(t *testing.T) {
ap := newTestAutoPath()
ctx := context.TODO()
for _, tc := range autopathNoAnswerTestCases {
m := tc.Msg()
rec := dnsrecorder.New(&test.ResponseWriter{})
rcode, err := ap.ServeDNS(ctx, rec, m)
if err != nil {
t.Errorf("expected no error, got %v\n", err)
continue
}
if plugin.ClientWrite(rcode) {
t.Fatalf("expected no client write, got one for rcode %d", rcode)
}
}
}
// nextHandler returns a Handler that returns an answer for the question in the
// request per the domain->answer map. On success an RR will be returned: "qname 3600 IN A 127.0.0.53"
func nextHandler(mm map[string]int) test.Handler {
return test.HandlerFunc(func(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
rcode, ok := mm[r.Question[0].Name]
if !ok {
return dns.RcodeServerFailure, nil
}
m := new(dns.Msg)
m.SetReply(r)
switch rcode {
case dns.RcodeNameError:
m.Rcode = rcode
m.Ns = []dns.RR{soa}
w.WriteMsg(m)
return m.Rcode, nil
case dns.RcodeSuccess:
m.Rcode = rcode
a, _ := dns.NewRR(r.Question[0].Name + defaultA)
m.Answer = []dns.RR{a}
w.WriteMsg(m)
return m.Rcode, nil
default:
panic("nextHandler: unhandled rcode")
}
})
}
const defaultA = " 3600 IN A 127.0.0.53"
var soa = func() dns.RR {
s, _ := dns.NewRR("example.org. 1800 IN SOA example.org. example.org. 1502165581 14400 3600 604800 14400")
return s
}()
func TestInSearchPath(t *testing.T) {
a := AutoPath{search: []string{"default.svc.cluster.local.", "svc.cluster.local.", "cluster.local."}}
tests := []struct {
qname string
b bool
}{
{"google.com", false},
{"default.svc.cluster.local.", true},
{"a.default.svc.cluster.local.", true},
{"a.b.svc.cluster.local.", false},
}
for i, tc := range tests {
got := firstInSearchPath(tc.qname, a.search)
if got != tc.b {
t.Errorf("Test %d, got %v, expected %v", i, got, tc.b)
}
}
}

25
plugin/autopath/cname.go Normal file
View File

@@ -0,0 +1,25 @@
package autopath
import (
"strings"
"github.com/miekg/dns"
)
// cnamer will prefix the answer section with a cname that points from original qname to the
// name of the first RR. It will also update the question section and put original in there.
func cnamer(m *dns.Msg, original string) {
for _, a := range m.Answer {
if strings.EqualFold(original, a.Header().Name) {
continue
}
m.Answer = append(m.Answer, nil)
copy(m.Answer[1:], m.Answer)
m.Answer[0] = &dns.CNAME{
Hdr: dns.RR_Header{Name: original, Rrtype: dns.TypeCNAME, Class: dns.ClassINET, Ttl: a.Header().Ttl},
Target: a.Header().Name,
}
break
}
m.Question[0].Name = original
}

93
plugin/autopath/setup.go Normal file
View File

@@ -0,0 +1,93 @@
package autopath
import (
"fmt"
"github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/erratic"
"github.com/coredns/coredns/plugin/kubernetes"
"github.com/mholt/caddy"
"github.com/miekg/dns"
)
func init() {
caddy.RegisterPlugin("autopath", caddy.Plugin{
ServerType: "dns",
Action: setup,
})
}
func setup(c *caddy.Controller) error {
ap, mw, err := autoPathParse(c)
if err != nil {
return plugin.Error("autopath", err)
}
// Do this in OnStartup, so all plugin has been initialized.
c.OnStartup(func() error {
m := dnsserver.GetConfig(c).Handler(mw)
if m == nil {
return nil
}
if x, ok := m.(*kubernetes.Kubernetes); ok {
ap.searchFunc = x.AutoPath
}
if x, ok := m.(*erratic.Erratic); ok {
ap.searchFunc = x.AutoPath
}
return nil
})
dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
ap.Next = next
return ap
})
return nil
}
// allowedMiddleware has a list of plugin that can be used by autopath.
var allowedMiddleware = map[string]bool{
"@kubernetes": true,
"@erratic": true,
}
func autoPathParse(c *caddy.Controller) (*AutoPath, string, error) {
ap := &AutoPath{}
mw := ""
for c.Next() {
zoneAndresolv := c.RemainingArgs()
if len(zoneAndresolv) < 1 {
return ap, "", fmt.Errorf("no resolv-conf specified")
}
resolv := zoneAndresolv[len(zoneAndresolv)-1]
if resolv[0] == '@' {
_, ok := allowedMiddleware[resolv]
if ok {
mw = resolv[1:]
}
} else {
// assume file on disk
rc, err := dns.ClientConfigFromFile(resolv)
if err != nil {
return ap, "", fmt.Errorf("failed to parse %q: %v", resolv, err)
}
ap.search = rc.Search
plugin.Zones(ap.search).Normalize()
ap.search = append(ap.search, "") // sentinal value as demanded.
}
ap.Zones = zoneAndresolv[:len(zoneAndresolv)-1]
if len(ap.Zones) == 0 {
ap.Zones = make([]string, len(c.ServerBlockKeys))
copy(ap.Zones, c.ServerBlockKeys)
}
for i, str := range ap.Zones {
ap.Zones[i] = plugin.Host(str).Normalize()
}
}
return ap, mw, nil
}

View File

@@ -0,0 +1,77 @@
package autopath
import (
"os"
"reflect"
"strings"
"testing"
"github.com/coredns/coredns/plugin/test"
"github.com/mholt/caddy"
)
func TestSetupAutoPath(t *testing.T) {
resolv, rm, err := test.TempFile(os.TempDir(), resolvConf)
if err != nil {
t.Fatalf("Could not create resolv.conf test file %s: %s", resolvConf, err)
}
defer rm()
tests := []struct {
input string
shouldErr bool
expectedZone string
expectedMw string // expected plugin.
expectedSearch []string // expected search path
expectedErrContent string // substring from the expected error. Empty for positive cases.
}{
// positive
{`autopath @kubernetes`, false, "", "kubernetes", nil, ""},
{`autopath example.org @kubernetes`, false, "example.org.", "kubernetes", nil, ""},
{`autopath 10.0.0.0/8 @kubernetes`, false, "10.in-addr.arpa.", "kubernetes", nil, ""},
{`autopath ` + resolv, false, "", "", []string{"bar.com.", "baz.com.", ""}, ""},
// negative
{`autopath kubernetes`, true, "", "", nil, "open kubernetes: no such file or directory"},
{`autopath`, true, "", "", nil, "no resolv-conf"},
}
for i, test := range tests {
c := caddy.NewTestController("dns", test.input)
ap, mw, err := autoPathParse(c)
if test.shouldErr && err == nil {
t.Errorf("Test %d: Expected error but found %s for input %s", i, err, test.input)
}
if err != nil {
if !test.shouldErr {
t.Errorf("Test %d: Expected no error but found one for input %s. Error was: %v", i, test.input, err)
}
if !strings.Contains(err.Error(), test.expectedErrContent) {
t.Errorf("Test %d: Expected error to contain: %v, found error: %v, input: %s", i, test.expectedErrContent, err, test.input)
}
}
if !test.shouldErr && mw != test.expectedMw {
t.Errorf("Test %d, Middleware not correctly set for input %s. Expected: %s, actual: %s", i, test.input, test.expectedMw, mw)
}
if !test.shouldErr && ap.search != nil {
if !reflect.DeepEqual(test.expectedSearch, ap.search) {
t.Errorf("Test %d, wrong searchpath for input %s. Expected: '%v', actual: '%v'", i, test.input, test.expectedSearch, ap.search)
}
}
if !test.shouldErr && test.expectedZone != "" {
if test.expectedZone != ap.Zones[0] {
t.Errorf("Test %d, expected zone %q for input %s, got: %q", i, test.expectedZone, test.input, ap.Zones[0])
}
}
}
}
const resolvConf = `nameserver 1.2.3.4
domain foo.com
search bar.com baz.com
options ndots:5
`