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:
Andri Yngvason
2026-08-20 03:27:03 +00:00
committed by GitHub
parent a53ecb7c2d
commit a1154dcee5
6 changed files with 982 additions and 0 deletions

View File

@@ -1,8 +1,11 @@
package template
import (
"context"
"reflect"
"regexp"
"strconv"
"strings"
gotmpl "text/template"
"github.com/coredns/caddy"
@@ -10,6 +13,10 @@ import (
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/pkg/upstream"
"github.com/expr-lang/expr"
"github.com/expr-lang/expr/ast"
"github.com/expr-lang/expr/builtin"
"github.com/expr-lang/expr/parser"
"github.com/miekg/dns"
)
@@ -17,6 +24,17 @@ import (
// OOM during regex compilation with malicious input.
const maxRegexpLen = 10000
var varNameRegexp = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
func isExprIdentifier(name string) bool {
tree, err := parser.Parse(name)
if err != nil {
return false
}
ident, ok := tree.Node.(*ast.IdentifierNode)
return ok && ident.Value == name
}
func init() { plugin.Register("template", setupTemplate) }
func setupTemplate(c *caddy.Controller) error {
@@ -63,6 +81,8 @@ func templateParse(c *caddy.Controller) (handler Handler, err error) {
t.answer = make([]*gotmpl.Template, 0)
t.upstream = upstream.New()
varEnv := exprEnv(context.Background(), nil, &templateData{})
for c.NextBlock() {
switch c.Val() {
case "match":
@@ -121,6 +141,41 @@ func templateParse(c *caddy.Controller) (handler Handler, err error) {
t.authority = append(t.authority, tmpl)
}
case "var":
args := c.RemainingArgs()
if len(args) < 2 {
return handler, c.ArgErr()
}
if !varNameRegexp.MatchString(args[0]) {
return handler, c.Errf("invalid variable name %q", args[0])
}
_, isEnv := varEnv[args[0]]
_, isBuiltin := builtin.Index[args[0]]
if isEnv || isBuiltin || !isExprIdentifier(args[0]) {
return handler, c.Errf("variable name %q is reserved", args[0])
}
prog, err := expr.Compile(strings.Join(args[1:], " "), expr.Env(varEnv), expr.DisableBuiltin("type"))
if err != nil {
return handler, c.Errf("could not compile expression: %s, %v", args[0], err)
}
if rt := prog.Node().Type(); rt == nil || rt.Kind() == reflect.Interface {
varEnv[args[0]] = new(any)
} else {
varEnv[args[0]] = reflect.Zero(rt).Interface()
}
t.vars = append(t.vars, variable{name: args[0], prog: prog})
case "expr":
args := c.RemainingArgs()
if len(args) == 0 {
return handler, c.ArgErr()
}
prog, err := expr.Compile(strings.Join(args, " "), expr.Env(varEnv), expr.DisableBuiltin("type"))
if err != nil {
return handler, c.Errf("could not compile expression: %v", err)
}
t.exprs = append(t.exprs, prog)
case "rcode":
if !c.NextArg() {
return handler, c.ArgErr()