This function complements the existing "templatefile" to deal with the
unusual situation of rendering a template that comes from somewhere
outside of the current module's source code, such as from a data resource
result.
We have some historical experience with the now-deprecated
hashicorp/template provider and its template_file data source, where we
found that new authors would find it via web search and assume it was
"the way" to render templates in Terraform, and then get frustrated
dealing with the confusing situation of writing a string template that
generates another string template for a second round of template rendering.
To try to support those who have this unusual need without creating another
attractive nuisance that would derail new authors, this function imposes
the artificial extra rule that its template argument may only be populated
using a single reference to a symbol defined elsewhere in the same module.
This is intended to entice folks trying to use this function for something
other than its intended purpose to refer to its documentation (once
written) and then hopefully learn what other Terraform language feature
they ought to have used instead.
The syntax restriction only goes one level deep, so particularly-determined
authors can still intentionally misuse this function by adding one level
of indirection, such as by building template source code in a local value
and then passing that local value as the template argument. The restriction
is in place only to reduce the chances of someone _misunderstanding_ the
purpose of this function; we don't intend to prevent someone from actively
deciding to misuse it, if they have a good reason to do so.
This new function inherits the same restriction as templatefile where it
does not allow recursively calling other template-rendering functions.
This is to dissuade from trying to use Terraform templates "at large",
since Terraform's template language is not designed for such uses. It would
be better to build a Terraform provider that wraps a more featureful
template system like Gonja if someone really does need advanced templating,
beyond Terraform's basic goals of being able to build small configuration
files, etc.
Because this function's intended purpose is rendering templates obtained
from elsewhere, this function also blocks calls to any of Terraform's
functions that would read from the filesystem of the computer where
Terraform is running. This is a small additional measure of isolation to
reduce the risk of an attacker somehow modifying a dynamically-fetched
template to inspire Terraform to write sensitive data from the host
computer into a location accessible to the same attacker, or similar.
This is currently only a language experiment and so will not yet be
available in stable releases of Terraform. Before stabilizing this and
committing to supporting it indefinitely we'll want to gather feedback on
whether this function actually meets the intended narrow set of use-cases
around dynamic template rendering.
@ -420,6 +420,13 @@ var DescriptionList = map[string]descriptionEntry{
Description:"`templatefile` reads the file at the given path and renders its content as a template using a supplied set of template variables.",
ParamDescription:[]string{"",""},
},
"templatestring":{
Description:"`templatestring` takes a string from elsewhere in the module and renders its content as a template using a supplied set of template variables.",
ParamDescription:[]string{
"a simple reference to a string value containing the template source code",
"object of variables to expose in the template scope",
},
},
"textdecodebase64":{
Description:"`textdecodebase64` function decodes a string that was previously Base64-encoded, and then interprets the result as characters in a specified character encoding.",
returncty.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.
forn:=rangectx.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.
returncty.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:=rangeexpr.Variables(){
root:=traversal.RootName()
if_,ok:=ctx.Variables[root];!ok{
returncty.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
`testdata/recursive.tmpl:1,3-16: Error in function call; Call to function "templatefile" failed: cannot recursively call templatefile from inside templatefile call.`,
`testdata/recursive.tmpl:1,3-16: Error in function call; Call to function "templatefile" failed: cannot recursively call templatefile from inside another template function.`,
`testdata/recursive_namespaced.tmpl:1,3-22: Error in function call; Call to function "core::templatefile" failed: cannot recursively call templatefile from inside templatefile call.`,
`testdata/recursive_namespaced.tmpl:1,3-22: Error in function call; Call to function "core::templatefile" failed: cannot recursively call templatefile from inside another template function.`,
0,"invalid template expression: templatestring is only for rendering templates retrieved dynamically from elsewhere; to treat the inner expression as template syntax, write the reference expression directly without any template interpolation syntax",
)
case*hclsyntax.TemplateExpr:
// This is the more general case of someone trying to write
// an inline template as the argument. In this case we'll
// distinguish between an entirely-literal template, which
// probably suggests someone was trying to escape their template
// for the function to consume, vs. a template with other
// sequences that suggests someone was just trying to write
// an inline template and so probably doesn't need to call
0,"invalid template expression: templatestring is only for rendering templates retrieved dynamically from elsewhere, and so does not support providing a literal template; consider using a template string expression instead",
0,"invalid template expression: templatestring is only for rendering templates retrieved dynamically from elsewhere; to render an inline template, consider using a plain template string expression",
)
}
default:
// Nothing else is allowed.
// Someone who really does want to construct a template dynamically
// can factor out that construction into a local value and refer
// to it in the templatestring call, but it's not really feasible
// to explain that clearly in a short error message so we'll deal
// with that option on the function's documentation page instead,
returncty.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.
forn:=rangectx.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.
returncty.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:=rangeexpr.Variables(){
root:=traversal.RootName()
if_,ok:=ctx.Variables[root];!ok{
returncty.DynamicVal,function.NewArgErrorf(1,"vars map does not contain key %q, referenced at %s",root,traversal[0].SourceRange())
}
}
givenFuncs,fsFuncs,templateFuncs:=funcsCb()// this callback indirection is to avoid chicken/egg problems
returncty.NilType,fmt.Errorf("cannot recursively call %s from inside another template function",plainName)
},
})
case!allowFS&&fsFuncs.Has(plainName):
// Note: for now this assumes that allowFS is false only for
// the templatestring function, and so mentions that name
// directly in the error message.
funcs[name]=function.New(&function.Spec{
Params:fn.Params(),
VarParam:fn.VarParam(),
Type:func(args[]cty.Value)(cty.Type,error){
returncty.NilType,fmt.Errorf("cannot use filesystem access functions like %s in templatestring templates; consider passing the function result as a template variable instead",plainName)
},
})
default:
funcs[name]=fn
}
}
ctx.Functions=funcs
val,diags:=expr.Value(ctx)
ifdiags.HasErrors(){
returncty.DynamicVal,diags
}
returnval,nil
}
}
// Replace searches a given string for another given substring,
// and replaces all occurences with a given replacement string.
`invalid template expression: templatestring is only for rendering templates retrieved dynamically from elsewhere, and so does not support providing a literal template; consider using a template string expression instead`,
`invalid template expression: templatestring is only for rendering templates retrieved dynamically from elsewhere; to render an inline template, consider using a plain template string expression`,
},
{
`"can't write %%{for x in things}a literal template%%{endfor}"`,
map[string]cty.Value{},
cty.ObjectVal(map[string]cty.Value{
"things":cty.ListVal([]cty.Value{cty.True}),
}),
cty.NilVal,
`invalid template expression: templatestring is only for rendering templates retrieved dynamically from elsewhere, and so does not support providing a literal template; consider using a template string expression instead`,
},
{
`"can't write %{for x in things}a literal template%{endfor}"`,
map[string]cty.Value{},
cty.ObjectVal(map[string]cty.Value{
"things":cty.ListVal([]cty.Value{cty.True}),
}),
cty.NilVal,
`invalid template expression: templatestring is only for rendering templates retrieved dynamically from elsewhere; to render an inline template, consider using a plain template string expression`,
`invalid template expression: templatestring is only for rendering templates retrieved dynamically from elsewhere; to treat the inner expression as template syntax, write the reference expression directly without any template interpolation syntax`,
},
{
`1 + 1`,
map[string]cty.Value{},
cty.ObjectVal(map[string]cty.Value{}),
cty.NilVal,
`invalid template expression: must be a direct reference to a single string from elsewhere, containing valid Terraform template syntax`,
`<templatestring argument>:1,8-15: Error in function call; Call to function "fsfunc" failed: cannot use filesystem access functions like fsfunc in templatestring templates; consider passing the function result as a template variable instead.`,
`<templatestring argument>:1,8-21: Error in function call; Call to function "core::fsfunc" failed: cannot use filesystem access functions like fsfunc in templatestring templates; consider passing the function result as a template variable instead.`,
`<templatestring argument>:1,8-21: Error in function call; Call to function "templatefunc" failed: cannot recursively call templatefunc from inside another template function.`,
`<templatestring argument>:1,8-27: Error in function call; Call to function "core::templatefunc" failed: cannot recursively call templatefunc from inside another template function.`,
// The template function error takes priority over the filesystem
// function error if calling a function that's in both categories.
`<templatestring argument>:1,8-23: Error in function call; Call to function "fstemplatefunc" failed: cannot recursively call fstemplatefunc from inside another template function.`,
// The template function error takes priority over the filesystem
// function error if calling a function that's in both categories.
`<templatestring argument>:1,8-29: Error in function call; Call to function "core::fstemplatefunc" failed: cannot recursively call fstemplatefunc from inside another template function.`,