mirror of
https://github.com/coredns/coredns.git
synced 2026-08-20 23:08:28 -04:00
Templates with expr-lang (#8450)
* plugin/template: Add expr-lang variables Signed-off-by: Andri Yngvason <andri@yngvason.is> * plugin/template: Add extra expressions that must match Signed-off-by: Andri Yngvason <andri@yngvason.is> * plugin/template: README: Add embedded device resolution example Signed-off-by: Andri Yngvason <andri@yngvason.is> --------- Signed-off-by: Andri Yngvason <andri@yngvason.is>
This commit is contained in:
293
plugin/template/expr_test.go
Normal file
293
plugin/template/expr_test.go
Normal file
@@ -0,0 +1,293 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/coredns/caddy"
|
||||
"github.com/coredns/coredns/plugin/metadata"
|
||||
"github.com/coredns/coredns/plugin/pkg/dnstest"
|
||||
"github.com/coredns/coredns/plugin/test"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func TestExpr(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config string
|
||||
qname string
|
||||
qtype uint16
|
||||
md map[string]string
|
||||
expectedCode int
|
||||
expectedAnswer []string
|
||||
}{
|
||||
{
|
||||
name: "True",
|
||||
config: `template IN A example. {
|
||||
expr name() == 'a.example.'
|
||||
answer "{{ .Name }} 60 IN A 10.0.0.1"
|
||||
}`,
|
||||
qname: "a.example.",
|
||||
expectedAnswer: []string{"a.example. 60 IN A 10.0.0.1"},
|
||||
},
|
||||
{
|
||||
name: "FalseServfail",
|
||||
config: `template IN A example. {
|
||||
expr name() == 'b.example.'
|
||||
answer "{{ .Name }} 60 IN A 10.0.0.1"
|
||||
}`,
|
||||
qname: "a.example.",
|
||||
expectedCode: dns.RcodeServerFailure,
|
||||
},
|
||||
{
|
||||
name: "FalseFallthrough",
|
||||
config: `template IN A example. {
|
||||
expr name() == 'b.example.'
|
||||
answer "{{ .Name }} 60 IN A 10.0.0.1"
|
||||
fallthrough
|
||||
}`,
|
||||
qname: "a.example.",
|
||||
expectedCode: rcodeFallthrough,
|
||||
},
|
||||
{
|
||||
name: "FalseFallthroughZoneMismatch",
|
||||
config: `template IN A example. {
|
||||
expr name() == 'b.example.'
|
||||
answer "{{ .Name }} 60 IN A 10.0.0.1"
|
||||
fallthrough other.example.
|
||||
}`,
|
||||
qname: "a.example.",
|
||||
expectedCode: dns.RcodeServerFailure,
|
||||
},
|
||||
{
|
||||
name: "FalseFallsToNextTemplate",
|
||||
config: `template IN A example. {
|
||||
expr name() == 'b.example.'
|
||||
answer "{{ .Name }} 60 IN A 10.0.0.1"
|
||||
fallthrough
|
||||
}
|
||||
template IN A example. {
|
||||
answer "{{ .Name }} 60 IN A 10.0.0.2"
|
||||
}`,
|
||||
qname: "a.example.",
|
||||
expectedAnswer: []string{"a.example. 60 IN A 10.0.0.2"},
|
||||
},
|
||||
{
|
||||
name: "AllTrue",
|
||||
config: `template IN A example. {
|
||||
expr name() == 'a.example.'
|
||||
expr type() == 'A'
|
||||
expr incidr(client_ip(), '10.0.0.0/8')
|
||||
answer "{{ .Name }} 60 IN A 10.0.0.1"
|
||||
}`,
|
||||
qname: "a.example.",
|
||||
expectedAnswer: []string{"a.example. 60 IN A 10.0.0.1"},
|
||||
},
|
||||
{
|
||||
name: "LastFalse",
|
||||
config: `template IN A example. {
|
||||
expr name() == 'a.example.'
|
||||
expr type() == 'A'
|
||||
expr incidr(client_ip(), '192.0.2.0/24')
|
||||
answer "{{ .Name }} 60 IN A 10.0.0.1"
|
||||
fallthrough
|
||||
}`,
|
||||
qname: "a.example.",
|
||||
expectedCode: rcodeFallthrough,
|
||||
},
|
||||
{
|
||||
name: "UsesVars",
|
||||
config: `template IN A example. {
|
||||
match ^(?P<n>[0-9]+)[.]example[.]$
|
||||
var num int(group('n'))
|
||||
expr num > 1
|
||||
answer "{{ .Name }} 60 IN A 10.0.0.{{ .Var.num }}"
|
||||
fallthrough
|
||||
}`,
|
||||
qname: "2.example.",
|
||||
expectedAnswer: []string{"2.example. 60 IN A 10.0.0.2"},
|
||||
},
|
||||
{
|
||||
name: "UsesVarsFalse",
|
||||
config: `template IN A example. {
|
||||
match ^(?P<n>[0-9]+)[.]example[.]$
|
||||
var num int(group('n'))
|
||||
expr num > 1
|
||||
answer "{{ .Name }} 60 IN A 10.0.0.{{ .Var.num }}"
|
||||
fallthrough
|
||||
}`,
|
||||
qname: "1.example.",
|
||||
expectedCode: rcodeFallthrough,
|
||||
},
|
||||
{
|
||||
name: "UsesGroup",
|
||||
config: `template IN A example. {
|
||||
match ^(?P<n>[0-9]+)[.]example[.]$
|
||||
expr group('n') == '2' && group(0) == '2.example.'
|
||||
answer "{{ .Name }} 60 IN A 10.0.0.{{ .Group.n }}"
|
||||
fallthrough
|
||||
}`,
|
||||
qname: "2.example.",
|
||||
expectedAnswer: []string{"2.example. 60 IN A 10.0.0.2"},
|
||||
},
|
||||
{
|
||||
name: "Metadata",
|
||||
config: `template IN A example. {
|
||||
expr metadata('test/region') == 'eu'
|
||||
answer "{{ .Name }} 60 IN A 10.0.0.1"
|
||||
fallthrough
|
||||
}`,
|
||||
qname: "a.example.",
|
||||
md: map[string]string{"test/region": "eu"},
|
||||
expectedAnswer: []string{"a.example. 60 IN A 10.0.0.1"},
|
||||
},
|
||||
{
|
||||
name: "MetadataMismatch",
|
||||
config: `template IN A example. {
|
||||
expr metadata('test/region') == 'eu'
|
||||
answer "{{ .Name }} 60 IN A 10.0.0.1"
|
||||
fallthrough
|
||||
}`,
|
||||
qname: "a.example.",
|
||||
md: map[string]string{"test/region": "us"},
|
||||
expectedCode: rcodeFallthrough,
|
||||
},
|
||||
{
|
||||
name: "NonBool",
|
||||
config: `template IN A example. {
|
||||
expr 1
|
||||
answer "{{ .Name }} 60 IN A 10.0.0.1"
|
||||
fallthrough
|
||||
}`,
|
||||
qname: "a.example.",
|
||||
expectedCode: rcodeFallthrough,
|
||||
},
|
||||
{
|
||||
name: "RuntimeError",
|
||||
config: `template IN A example. {
|
||||
expr incidr('notanip', '10.0.0.0/8')
|
||||
answer "{{ .Name }} 60 IN A 10.0.0.1"
|
||||
fallthrough
|
||||
}`,
|
||||
qname: "a.example.",
|
||||
expectedCode: dns.RcodeServerFailure,
|
||||
},
|
||||
{
|
||||
name: "NoMatchNoEvaluation",
|
||||
config: `template IN A example. {
|
||||
match ^a[.]example[.]$
|
||||
expr incidr('notanip', '10.0.0.0/8')
|
||||
answer "{{ .Name }} 60 IN A 10.0.0.1"
|
||||
fallthrough
|
||||
}`,
|
||||
qname: "b.example.",
|
||||
expectedCode: rcodeFallthrough,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
c := caddy.NewTestController("dns", tc.config)
|
||||
handler, err := templateParse(c)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no config error, got: %v", err)
|
||||
}
|
||||
handler.Next = test.NextHandler(rcodeFallthrough, nil)
|
||||
|
||||
ctx := context.Background()
|
||||
if tc.md != nil {
|
||||
ctx = metadata.ContextWithMetadata(ctx)
|
||||
for k, v := range tc.md {
|
||||
value := v
|
||||
metadata.SetValueFunc(ctx, k, func() string { return value })
|
||||
}
|
||||
}
|
||||
|
||||
qtype := tc.qtype
|
||||
if qtype == 0 {
|
||||
qtype = dns.TypeA
|
||||
}
|
||||
req := &dns.Msg{Question: []dns.Question{{Name: tc.qname, Qclass: dns.ClassINET, Qtype: qtype}}}
|
||||
rec := dnstest.NewRecorder(&test.ResponseWriter{})
|
||||
|
||||
code, err := handler.ServeDNS(ctx, rec, req)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got: %v", err)
|
||||
}
|
||||
if code != tc.expectedCode {
|
||||
t.Fatalf("expected rcode %v, got %v", tc.expectedCode, code)
|
||||
}
|
||||
if tc.expectedCode != dns.RcodeSuccess {
|
||||
return
|
||||
}
|
||||
|
||||
verifySection(t, "answer", rec.Msg.Answer, tc.expectedAnswer)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExprMatch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config string
|
||||
qname string
|
||||
expectedMatch bool
|
||||
expectedFthrough bool
|
||||
}{
|
||||
{
|
||||
name: "AllTrue",
|
||||
config: `template IN A example. {
|
||||
expr name() == 'a.example.'
|
||||
expr true
|
||||
fallthrough
|
||||
}`,
|
||||
qname: "a.example.",
|
||||
expectedMatch: true,
|
||||
},
|
||||
{
|
||||
name: "FirstFalse",
|
||||
config: `template IN A example. {
|
||||
expr false
|
||||
expr true
|
||||
fallthrough
|
||||
}`,
|
||||
qname: "a.example.",
|
||||
expectedFthrough: true,
|
||||
},
|
||||
{
|
||||
name: "FalseWithoutFallthrough",
|
||||
config: `template IN A example. {
|
||||
expr false
|
||||
}`,
|
||||
qname: "a.example.",
|
||||
},
|
||||
{
|
||||
name: "ErrorWithFallthrough",
|
||||
config: `template IN A example. {
|
||||
expr incidr('notanip', '10.0.0.0/8')
|
||||
fallthrough
|
||||
}`,
|
||||
qname: "a.example.",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
c := caddy.NewTestController("dns", tc.config)
|
||||
handler, err := templateParse(c)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no config error, got: %v", err)
|
||||
}
|
||||
|
||||
req := &dns.Msg{Question: []dns.Question{{Name: tc.qname, Qclass: dns.ClassINET, Qtype: dns.TypeA}}}
|
||||
_, match, fthrough := handler.Templates[0].match(context.Background(), requestFor(req))
|
||||
if match != tc.expectedMatch {
|
||||
t.Fatalf("expected match %v, got %v", tc.expectedMatch, match)
|
||||
}
|
||||
if fthrough != tc.expectedFthrough {
|
||||
t.Fatalf("expected fallthrough %v, got %v", tc.expectedFthrough, fthrough)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user