plugin/template: support metadata (#2958)
* Enable use of metadata in templates * Update README * Don't stash away ctx, instead use a new func
This commit is contained in:
parent
2faad5b397
commit
7cf73cc01d
4 changed files with 135 additions and 4 deletions
|
@ -82,6 +82,16 @@ func Labels(ctx context.Context) []string {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValueFuncs returns the map[string]Func from the context, or nil if it does not exist.
|
||||||
|
func ValueFuncs(ctx context.Context) map[string]Func {
|
||||||
|
if metadata := ctx.Value(key{}); metadata != nil {
|
||||||
|
if m, ok := metadata.(md); ok {
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ValueFunc returns the value function of label. If none can be found nil is returned. Calling the
|
// ValueFunc returns the value function of label. If none can be found nil is returned. Calling the
|
||||||
// function returns the value of the label.
|
// function returns the value of the label.
|
||||||
func ValueFunc(ctx context.Context, label string) Func {
|
func ValueFunc(ctx context.Context, label string) Func {
|
||||||
|
|
|
@ -48,6 +48,8 @@ Each resource record is a full-featured [Go template](https://golang.org/pkg/tex
|
||||||
* `.Group` a map of the named capture groups.
|
* `.Group` a map of the named capture groups.
|
||||||
* `.Message` the complete incoming DNS message.
|
* `.Message` the complete incoming DNS message.
|
||||||
* `.Question` the matched question section.
|
* `.Question` the matched question section.
|
||||||
|
* `.Meta` a function that takes a metadata name and returns the value, if the
|
||||||
|
metadata plugin is enabled. For example, `.Meta "kubernetes/client-namespace"`
|
||||||
|
|
||||||
The output of the template must be a [RFC 1035](https://tools.ietf.org/html/rfc1035) style resource record (commonly referred to as a "zone file").
|
The output of the template must be a [RFC 1035](https://tools.ietf.org/html/rfc1035) style resource record (commonly referred to as a "zone file").
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
gotmpl "text/template"
|
gotmpl "text/template"
|
||||||
|
|
||||||
"github.com/coredns/coredns/plugin"
|
"github.com/coredns/coredns/plugin"
|
||||||
|
"github.com/coredns/coredns/plugin/metadata"
|
||||||
"github.com/coredns/coredns/plugin/metrics"
|
"github.com/coredns/coredns/plugin/metrics"
|
||||||
"github.com/coredns/coredns/plugin/pkg/fall"
|
"github.com/coredns/coredns/plugin/pkg/fall"
|
||||||
"github.com/coredns/coredns/plugin/pkg/upstream"
|
"github.com/coredns/coredns/plugin/pkg/upstream"
|
||||||
|
@ -47,6 +48,19 @@ type templateData struct {
|
||||||
Type string
|
Type string
|
||||||
Message *dns.Msg
|
Message *dns.Msg
|
||||||
Question *dns.Question
|
Question *dns.Question
|
||||||
|
md map[string]metadata.Func
|
||||||
|
}
|
||||||
|
|
||||||
|
func (data *templateData) Meta(metaName string) string {
|
||||||
|
if data.md == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if f, ok := data.md[metaName]; ok {
|
||||||
|
return f()
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeDNS implements the plugin.Handler interface.
|
// ServeDNS implements the plugin.Handler interface.
|
||||||
|
@ -59,7 +73,7 @@ func (h Handler) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, template := range h.Templates {
|
for _, template := range h.Templates {
|
||||||
data, match, fthrough := template.match(state, zone)
|
data, match, fthrough := template.match(ctx, state, zone)
|
||||||
if !match {
|
if !match {
|
||||||
if !fthrough {
|
if !fthrough {
|
||||||
return dns.RcodeNameError, nil
|
return dns.RcodeNameError, nil
|
||||||
|
@ -114,7 +128,7 @@ func (h Handler) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg)
|
||||||
// Name implements the plugin.Handler interface.
|
// Name implements the plugin.Handler interface.
|
||||||
func (h Handler) Name() string { return "template" }
|
func (h Handler) Name() string { return "template" }
|
||||||
|
|
||||||
func executeRRTemplate(server, section string, template *gotmpl.Template, data templateData) (dns.RR, error) {
|
func executeRRTemplate(server, section string, template *gotmpl.Template, data *templateData) (dns.RR, error) {
|
||||||
buffer := &bytes.Buffer{}
|
buffer := &bytes.Buffer{}
|
||||||
err := template.Execute(buffer, data)
|
err := template.Execute(buffer, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -129,9 +143,9 @@ func executeRRTemplate(server, section string, template *gotmpl.Template, data t
|
||||||
return rr, nil
|
return rr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t template) match(state request.Request, zone string) (templateData, bool, bool) {
|
func (t template) match(ctx context.Context, state request.Request, zone string) (*templateData, bool, bool) {
|
||||||
q := state.Req.Question[0]
|
q := state.Req.Question[0]
|
||||||
data := templateData{}
|
data := &templateData{md: metadata.ValueFuncs(ctx)}
|
||||||
|
|
||||||
zone = plugin.Zones(t.zones).Matches(state.Name())
|
zone = plugin.Zones(t.zones).Matches(state.Name())
|
||||||
if zone == "" {
|
if zone == "" {
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
gotmpl "text/template"
|
gotmpl "text/template"
|
||||||
|
|
||||||
|
"github.com/coredns/coredns/plugin/metadata"
|
||||||
"github.com/coredns/coredns/plugin/pkg/dnstest"
|
"github.com/coredns/coredns/plugin/pkg/dnstest"
|
||||||
"github.com/coredns/coredns/plugin/pkg/fall"
|
"github.com/coredns/coredns/plugin/pkg/fall"
|
||||||
"github.com/coredns/coredns/plugin/test"
|
"github.com/coredns/coredns/plugin/test"
|
||||||
|
@ -100,6 +101,24 @@ func TestHandler(t *testing.T) {
|
||||||
fall: fall.Root,
|
fall: fall.Root,
|
||||||
zones: []string{"."},
|
zones: []string{"."},
|
||||||
}
|
}
|
||||||
|
mdTemplate := template{
|
||||||
|
regex: []*regexp.Regexp{regexp.MustCompile("(^|[.])ip-10-(?P<b>[0-9]*)-(?P<c>[0-9]*)-(?P<d>[0-9]*)[.]example[.]$")},
|
||||||
|
answer: []*gotmpl.Template{gotmpl.Must(gotmpl.New("answer").Parse(`{{ .Meta "foo" }}-{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}`))},
|
||||||
|
additional: []*gotmpl.Template{gotmpl.Must(gotmpl.New("additional").Parse(`{{ .Meta "bar" }}.example. IN A 203.0.113.8`))},
|
||||||
|
authority: []*gotmpl.Template{gotmpl.Must(gotmpl.New("authority").Parse(`example. IN NS {{ .Meta "bar" }}.example.com.`))},
|
||||||
|
qclass: dns.ClassANY,
|
||||||
|
qtype: dns.TypeANY,
|
||||||
|
fall: fall.Root,
|
||||||
|
zones: []string{"."},
|
||||||
|
}
|
||||||
|
mdMissingTemplate := template{
|
||||||
|
regex: []*regexp.Regexp{regexp.MustCompile("(^|[.])ip-10-(?P<b>[0-9]*)-(?P<c>[0-9]*)-(?P<d>[0-9]*)[.]example[.]$")},
|
||||||
|
answer: []*gotmpl.Template{gotmpl.Must(gotmpl.New("answer").Parse(`{{ .Meta "foofoo" }}{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}`))},
|
||||||
|
qclass: dns.ClassANY,
|
||||||
|
qtype: dns.TypeANY,
|
||||||
|
fall: fall.Root,
|
||||||
|
zones: []string{"."},
|
||||||
|
}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
tmpl template
|
tmpl template
|
||||||
|
@ -110,6 +129,7 @@ func TestHandler(t *testing.T) {
|
||||||
expectedCode int
|
expectedCode int
|
||||||
expectedErr string
|
expectedErr string
|
||||||
verifyResponse func(*dns.Msg) error
|
verifyResponse func(*dns.Msg) error
|
||||||
|
md map[string]string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "RcodeServFail",
|
name: "RcodeServFail",
|
||||||
|
@ -276,6 +296,78 @@ func TestHandler(t *testing.T) {
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "mdMatch",
|
||||||
|
tmpl: mdTemplate,
|
||||||
|
qclass: dns.ClassINET,
|
||||||
|
qtype: dns.TypeA,
|
||||||
|
qname: "ip-10-95-12-8.example.",
|
||||||
|
verifyResponse: func(r *dns.Msg) error {
|
||||||
|
if len(r.Answer) != 1 {
|
||||||
|
return fmt.Errorf("expected 1 answer, got %v", len(r.Answer))
|
||||||
|
}
|
||||||
|
if r.Answer[0].Header().Rrtype != dns.TypeA {
|
||||||
|
return fmt.Errorf("expected an A record answer, got %v", dns.TypeToString[r.Answer[0].Header().Rrtype])
|
||||||
|
}
|
||||||
|
name := "myfoo-ip-10-95-12-8.example."
|
||||||
|
if r.Answer[0].Header().Name != name {
|
||||||
|
return fmt.Errorf("expected answer name %q, got %q", name, r.Answer[0].Header().Name)
|
||||||
|
}
|
||||||
|
if len(r.Extra) != 1 {
|
||||||
|
return fmt.Errorf("expected 1 extra record, got %v", len(r.Extra))
|
||||||
|
}
|
||||||
|
if r.Extra[0].Header().Rrtype != dns.TypeA {
|
||||||
|
return fmt.Errorf("expected an additional A record, got %v", dns.TypeToString[r.Extra[0].Header().Rrtype])
|
||||||
|
}
|
||||||
|
name = "mybar.example."
|
||||||
|
if r.Extra[0].Header().Name != name {
|
||||||
|
return fmt.Errorf("expected additional name %q, got %q", name, r.Extra[0].Header().Name)
|
||||||
|
}
|
||||||
|
if len(r.Ns) != 1 {
|
||||||
|
return fmt.Errorf("expected 1 authoritative record, got %v", len(r.Extra))
|
||||||
|
}
|
||||||
|
if r.Ns[0].Header().Rrtype != dns.TypeNS {
|
||||||
|
return fmt.Errorf("expected an authoritative NS record, got %v", dns.TypeToString[r.Extra[0].Header().Rrtype])
|
||||||
|
}
|
||||||
|
ns, ok := r.Ns[0].(*dns.NS)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("expected NS record to be type NS, got %v", r.Ns[0])
|
||||||
|
}
|
||||||
|
rdata := "mybar.example.com."
|
||||||
|
if ns.Ns != rdata {
|
||||||
|
return fmt.Errorf("expected ns rdata %q, got %q", rdata, ns.Ns)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
md: map[string]string{
|
||||||
|
"foo": "myfoo",
|
||||||
|
"bar": "mybar",
|
||||||
|
"foobar": "myfoobar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mdMissing",
|
||||||
|
tmpl: mdMissingTemplate,
|
||||||
|
qclass: dns.ClassINET,
|
||||||
|
qtype: dns.TypeA,
|
||||||
|
qname: "ip-10-95-12-8.example.",
|
||||||
|
verifyResponse: func(r *dns.Msg) error {
|
||||||
|
if len(r.Answer) != 1 {
|
||||||
|
return fmt.Errorf("expected 1 answer, got %v", len(r.Answer))
|
||||||
|
}
|
||||||
|
if r.Answer[0].Header().Rrtype != dns.TypeA {
|
||||||
|
return fmt.Errorf("expected an A record answer, got %v", dns.TypeToString[r.Answer[0].Header().Rrtype])
|
||||||
|
}
|
||||||
|
name := "ip-10-95-12-8.example."
|
||||||
|
if r.Answer[0].Header().Name != name {
|
||||||
|
return fmt.Errorf("expected answer name %q, got %q", name, r.Answer[0].Header().Name)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
md: map[string]string{
|
||||||
|
"foo": "myfoo",
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := context.TODO()
|
ctx := context.TODO()
|
||||||
|
@ -294,6 +386,19 @@ func TestHandler(t *testing.T) {
|
||||||
}},
|
}},
|
||||||
}
|
}
|
||||||
rec := dnstest.NewRecorder(&test.ResponseWriter{})
|
rec := dnstest.NewRecorder(&test.ResponseWriter{})
|
||||||
|
if tr.md != nil {
|
||||||
|
ctx = metadata.ContextWithMetadata(context.Background())
|
||||||
|
|
||||||
|
for k, v := range tr.md {
|
||||||
|
// Go requires copying to a local variable for the closure to work
|
||||||
|
kk := k
|
||||||
|
vv := v
|
||||||
|
metadata.SetValueFunc(ctx, kk, func() string {
|
||||||
|
return vv
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
code, err := handler.ServeDNS(ctx, rec, req)
|
code, err := handler.ServeDNS(ctx, rec, req)
|
||||||
if err == nil && tr.expectedErr != "" {
|
if err == nil && tr.expectedErr != "" {
|
||||||
t.Errorf("Test %v expected error: %v, got nothing", tr.name, tr.expectedErr)
|
t.Errorf("Test %v expected error: %v, got nothing", tr.name, tr.expectedErr)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue