diff --git a/plugin/metadata/provider.go b/plugin/metadata/provider.go index b22064200..309d304b7 100644 --- a/plugin/metadata/provider.go +++ b/plugin/metadata/provider.go @@ -82,6 +82,16 @@ func Labels(ctx context.Context) []string { 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 // function returns the value of the label. func ValueFunc(ctx context.Context, label string) Func { diff --git a/plugin/template/README.md b/plugin/template/README.md index 993fec8b7..64af37f93 100644 --- a/plugin/template/README.md +++ b/plugin/template/README.md @@ -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. * `.Message` the complete incoming DNS message. * `.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"). diff --git a/plugin/template/template.go b/plugin/template/template.go index 1c9f61622..eaa291f7f 100644 --- a/plugin/template/template.go +++ b/plugin/template/template.go @@ -8,6 +8,7 @@ import ( gotmpl "text/template" "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/metadata" "github.com/coredns/coredns/plugin/metrics" "github.com/coredns/coredns/plugin/pkg/fall" "github.com/coredns/coredns/plugin/pkg/upstream" @@ -47,6 +48,19 @@ type templateData struct { Type string Message *dns.Msg 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. @@ -59,7 +73,7 @@ func (h Handler) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) } for _, template := range h.Templates { - data, match, fthrough := template.match(state, zone) + data, match, fthrough := template.match(ctx, state, zone) if !match { if !fthrough { 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. 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{} err := template.Execute(buffer, data) if err != nil { @@ -129,9 +143,9 @@ func executeRRTemplate(server, section string, template *gotmpl.Template, data t 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] - data := templateData{} + data := &templateData{md: metadata.ValueFuncs(ctx)} zone = plugin.Zones(t.zones).Matches(state.Name()) if zone == "" { diff --git a/plugin/template/template_test.go b/plugin/template/template_test.go index d2869b53b..1aa26229f 100644 --- a/plugin/template/template_test.go +++ b/plugin/template/template_test.go @@ -7,6 +7,7 @@ import ( "testing" gotmpl "text/template" + "github.com/coredns/coredns/plugin/metadata" "github.com/coredns/coredns/plugin/pkg/dnstest" "github.com/coredns/coredns/plugin/pkg/fall" "github.com/coredns/coredns/plugin/test" @@ -100,6 +101,24 @@ func TestHandler(t *testing.T) { fall: fall.Root, zones: []string{"."}, } + mdTemplate := template{ + regex: []*regexp.Regexp{regexp.MustCompile("(^|[.])ip-10-(?P[0-9]*)-(?P[0-9]*)-(?P[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[0-9]*)-(?P[0-9]*)-(?P[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 { tmpl template @@ -110,6 +129,7 @@ func TestHandler(t *testing.T) { expectedCode int expectedErr string verifyResponse func(*dns.Msg) error + md map[string]string }{ { name: "RcodeServFail", @@ -276,6 +296,78 @@ func TestHandler(t *testing.T) { 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() @@ -294,6 +386,19 @@ func TestHandler(t *testing.T) { }}, } 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) if err == nil && tr.expectedErr != "" { t.Errorf("Test %v expected error: %v, got nothing", tr.name, tr.expectedErr)