141 lines
4.8 KiB
Go
141 lines
4.8 KiB
Go
package function
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/hashicorp/go-cty-funcs/filesystem"
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/hashicorp/hcl/v2/hclsyntax"
|
|
"github.com/zclconf/go-cty/cty"
|
|
"github.com/zclconf/go-cty/cty/function"
|
|
)
|
|
|
|
// MakeTemplateFileFunc constructs a function that takes a file path and
|
|
// an arbitrary object of named values and attempts to render the referenced
|
|
// file as a template using HCL template syntax.
|
|
//
|
|
// The template itself may recursively call other functions so a callback
|
|
// must be provided to get access to those functions. The template cannot,
|
|
// however, access any variables defined in the scope: it is restricted only to
|
|
// those variables provided in the second function argument.
|
|
//
|
|
// As a special exception, a referenced template file may not recursively call
|
|
// the templatefile function, since that would risk the same file being
|
|
// included into itself indefinitely.
|
|
func MakeTemplateFileFunc(baseDir string, funcsCb func() map[string]function.Function) function.Function {
|
|
|
|
params := []function.Parameter{
|
|
{
|
|
Name: "path",
|
|
Type: cty.String,
|
|
},
|
|
{
|
|
Name: "vars",
|
|
Type: cty.DynamicPseudoType,
|
|
},
|
|
}
|
|
|
|
loadTmpl := func(fn string) (hcl.Expression, error) {
|
|
// We re-use File here to ensure the same filename interpretation
|
|
// as it does, along with its other safety checks.
|
|
tmplVal, err := filesystem.File(baseDir, cty.StringVal(fn))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
expr, diags := hclsyntax.ParseTemplate([]byte(tmplVal.AsString()), fn, hcl.Pos{Line: 1, Column: 1})
|
|
if diags.HasErrors() {
|
|
return nil, diags
|
|
}
|
|
|
|
return expr, nil
|
|
}
|
|
|
|
renderTmpl := func(expr hcl.Expression, varsVal cty.Value) (cty.Value, error) {
|
|
if varsTy := varsVal.Type(); !(varsTy.IsMapType() || varsTy.IsObjectType()) {
|
|
return cty.DynamicVal, function.NewArgErrorf(1, "invalid vars value: must be a map") // or an object, but we don't strongly distinguish these most of the time
|
|
}
|
|
|
|
ctx := &hcl.EvalContext{
|
|
Variables: varsVal.AsValueMap(),
|
|
}
|
|
|
|
// We require all of the variables to be valid HCL identifiers, because
|
|
// otherwise there would be no way to refer to them in the template
|
|
// anyway. Rejecting this here gives better feedback to the user
|
|
// than a syntax error somewhere in the template itself.
|
|
for n := range ctx.Variables {
|
|
if !hclsyntax.ValidIdentifier(n) {
|
|
// This error message intentionally doesn't describe _all_ of
|
|
// the different permutations that are technically valid as an
|
|
// HCL identifier, but rather focuses on what we might
|
|
// consider to be an "idiomatic" variable name.
|
|
return cty.DynamicVal, function.NewArgErrorf(1, "invalid template variable name %q: must start with a letter, followed by zero or more letters, digits, and underscores", n)
|
|
}
|
|
}
|
|
|
|
// We'll pre-check references in the template here so we can give a
|
|
// more specialized error message than HCL would by default, so it's
|
|
// clearer that this problem is coming from a templatefile call.
|
|
for _, traversal := range expr.Variables() {
|
|
root := traversal.RootName()
|
|
if _, ok := ctx.Variables[root]; !ok {
|
|
return cty.DynamicVal, function.NewArgErrorf(1, "vars map does not contain key %q, referenced at %s", root, traversal[0].SourceRange())
|
|
}
|
|
}
|
|
|
|
givenFuncs := funcsCb() // this callback indirection is to avoid chicken/egg problems
|
|
funcs := make(map[string]function.Function, len(givenFuncs))
|
|
for name, fn := range givenFuncs {
|
|
if name == "templatefile" {
|
|
// We stub this one out to prevent recursive calls.
|
|
funcs[name] = function.New(&function.Spec{
|
|
Params: params,
|
|
Type: func(args []cty.Value) (cty.Type, error) {
|
|
return cty.NilType, fmt.Errorf("cannot recursively call templatefile from inside templatefile call")
|
|
},
|
|
})
|
|
continue
|
|
}
|
|
funcs[name] = fn
|
|
}
|
|
ctx.Functions = funcs
|
|
|
|
val, diags := expr.Value(ctx)
|
|
if diags.HasErrors() {
|
|
return cty.DynamicVal, diags
|
|
}
|
|
return val, nil
|
|
}
|
|
|
|
return function.New(&function.Spec{
|
|
Params: params,
|
|
Type: func(args []cty.Value) (cty.Type, error) {
|
|
if !(args[0].IsKnown() && args[1].IsKnown()) {
|
|
return cty.DynamicPseudoType, nil
|
|
}
|
|
|
|
// We'll render our template now to see what result type it
|
|
// produces. A template consisting only of a single interpolation
|
|
// can potentially return any type.
|
|
expr, err := loadTmpl(args[0].AsString())
|
|
if err != nil {
|
|
return cty.DynamicPseudoType, err
|
|
}
|
|
|
|
// This is safe even if args[1] contains unknowns because the HCL
|
|
// template renderer itself knows how to short-circuit those.
|
|
val, err := renderTmpl(expr, args[1])
|
|
return val.Type(), err
|
|
},
|
|
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
|
|
expr, err := loadTmpl(args[0].AsString())
|
|
if err != nil {
|
|
return cty.DynamicVal, err
|
|
}
|
|
return renderTmpl(expr, args[1])
|
|
},
|
|
})
|
|
|
|
}
|