mirror of
https://github.com/hashicorp/terraform.git
synced 2026-02-03 20:50:59 -05:00
The templatestring function has some special constraints on its first
argument that are included to add some intentional friction for those who
are new to Terraform, want to do some simple template rendering, but have
only found the templatestring function so far.
We know from previous experience with the hashicorp/template provider that
this sort of functionality tends to attract those who haven't yet learned
that the Terraform language has built-in support for string templates
(without calling any function), who would then get confused by the need
for an extra level of escaping to render a template string only indirectly
through this function.
However, this rule is not intended to be onerous and require writing the
rest of the containing module in an unnatural way to work around it, so
here we loosen the rule to allow some additional forms:
- An index expression whose collection operand meets these rules.
- A relative traversal whose source operand meets these rules.
In particular this makes it possible to write an expression like:
data.example.example[each.key].result
...which is a relative traversal from an index from a scope traversal,
and is a very reasonable thing to write if you've retrieved multiple
templates using a data resource that uses for_each.
This also treats splat expressions in the same way as index expressions
at the static check stage, but that's only to allow us to reach the
dynamic type check that will ultimately report that a string is required,
because the result of a splat expression is a tuple. The type-related
error message is (subjectively) more helpful/relevant than the
syntax-related one for this case.
Finally, this includes some revisions to the documentation for this
function to correct some editing errors from the first pass and to slightly
loosen the language about what's allowed. It's still a little vague about
what exactly is allowed, but I'm doubtful that a precise definition in
terms of HCL's expression types would be very enlightening for a typical
reader anyway. We can tweak the specificity of the language here if we
start to see repeated questions about what is and is not valid.
444 lines
17 KiB
Go
444 lines
17 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package funcs
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/hashicorp/hcl/v2/ext/customdecode"
|
|
"github.com/hashicorp/hcl/v2/hclsyntax"
|
|
"github.com/zclconf/go-cty/cty"
|
|
"github.com/zclconf/go-cty/cty/convert"
|
|
"github.com/zclconf/go-cty/cty/function"
|
|
|
|
"github.com/hashicorp/terraform/internal/collections"
|
|
)
|
|
|
|
// StartsWithFunc constructs a function that checks if a string starts with
|
|
// a specific prefix using strings.HasPrefix
|
|
var StartsWithFunc = function.New(&function.Spec{
|
|
Params: []function.Parameter{
|
|
{
|
|
Name: "str",
|
|
Type: cty.String,
|
|
AllowUnknown: true,
|
|
},
|
|
{
|
|
Name: "prefix",
|
|
Type: cty.String,
|
|
},
|
|
},
|
|
Type: function.StaticReturnType(cty.Bool),
|
|
RefineResult: refineNotNull,
|
|
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
|
|
prefix := args[1].AsString()
|
|
|
|
if !args[0].IsKnown() {
|
|
// If the unknown value has a known prefix then we might be
|
|
// able to still produce a known result.
|
|
if prefix == "" {
|
|
// The empty string is a prefix of any string.
|
|
return cty.True, nil
|
|
}
|
|
if knownPrefix := args[0].Range().StringPrefix(); knownPrefix != "" {
|
|
if strings.HasPrefix(knownPrefix, prefix) {
|
|
return cty.True, nil
|
|
}
|
|
if len(knownPrefix) >= len(prefix) {
|
|
// If the prefix we're testing is no longer than the known
|
|
// prefix and it didn't match then the full string with
|
|
// that same prefix can't match either.
|
|
return cty.False, nil
|
|
}
|
|
}
|
|
return cty.UnknownVal(cty.Bool), nil
|
|
}
|
|
|
|
str := args[0].AsString()
|
|
|
|
if strings.HasPrefix(str, prefix) {
|
|
return cty.True, nil
|
|
}
|
|
|
|
return cty.False, nil
|
|
},
|
|
})
|
|
|
|
// EndsWithFunc constructs a function that checks if a string ends with
|
|
// a specific suffix using strings.HasSuffix
|
|
var EndsWithFunc = function.New(&function.Spec{
|
|
Params: []function.Parameter{
|
|
{
|
|
Name: "str",
|
|
Type: cty.String,
|
|
},
|
|
{
|
|
Name: "suffix",
|
|
Type: cty.String,
|
|
},
|
|
},
|
|
Type: function.StaticReturnType(cty.Bool),
|
|
RefineResult: refineNotNull,
|
|
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
|
|
str := args[0].AsString()
|
|
suffix := args[1].AsString()
|
|
|
|
if strings.HasSuffix(str, suffix) {
|
|
return cty.True, nil
|
|
}
|
|
|
|
return cty.False, nil
|
|
},
|
|
})
|
|
|
|
// ReplaceFunc constructs a function that searches a given string for another
|
|
// given substring, and replaces each occurence with a given replacement string.
|
|
var ReplaceFunc = function.New(&function.Spec{
|
|
Params: []function.Parameter{
|
|
{
|
|
Name: "str",
|
|
Type: cty.String,
|
|
},
|
|
{
|
|
Name: "substr",
|
|
Type: cty.String,
|
|
},
|
|
{
|
|
Name: "replace",
|
|
Type: cty.String,
|
|
},
|
|
},
|
|
Type: function.StaticReturnType(cty.String),
|
|
RefineResult: refineNotNull,
|
|
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
|
|
str := args[0].AsString()
|
|
substr := args[1].AsString()
|
|
replace := args[2].AsString()
|
|
|
|
// We search/replace using a regexp if the string is surrounded
|
|
// in forward slashes.
|
|
if len(substr) > 1 && substr[0] == '/' && substr[len(substr)-1] == '/' {
|
|
re, err := regexp.Compile(substr[1 : len(substr)-1])
|
|
if err != nil {
|
|
return cty.UnknownVal(cty.String), err
|
|
}
|
|
|
|
return cty.StringVal(re.ReplaceAllString(str, replace)), nil
|
|
}
|
|
|
|
return cty.StringVal(strings.Replace(str, substr, replace, -1)), nil
|
|
},
|
|
})
|
|
|
|
// StrContainsFunc searches a given string for another given substring,
|
|
// if found the function returns true, otherwise returns false.
|
|
var StrContainsFunc = function.New(&function.Spec{
|
|
Params: []function.Parameter{
|
|
{
|
|
Name: "str",
|
|
Type: cty.String,
|
|
},
|
|
{
|
|
Name: "substr",
|
|
Type: cty.String,
|
|
},
|
|
},
|
|
Type: function.StaticReturnType(cty.Bool),
|
|
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
|
|
str := args[0].AsString()
|
|
substr := args[1].AsString()
|
|
|
|
if strings.Contains(str, substr) {
|
|
return cty.True, nil
|
|
}
|
|
|
|
return cty.False, nil
|
|
},
|
|
})
|
|
|
|
// TemplateStringFunc renders a template presented either as a literal string
|
|
// or as a reference to a string from elsewhere.
|
|
func MakeTemplateStringFunc(funcsCb func() (funcs map[string]function.Function, fsFuncs collections.Set[string], templateFuncs collections.Set[string])) function.Function {
|
|
return function.New(&function.Spec{
|
|
Params: []function.Parameter{
|
|
{
|
|
Name: "template",
|
|
Type: customdecode.ExpressionClosureType,
|
|
},
|
|
{
|
|
Name: "vars",
|
|
Type: cty.DynamicPseudoType,
|
|
},
|
|
},
|
|
Type: function.StaticReturnType(cty.String),
|
|
RefineResult: refineNotNull,
|
|
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
|
|
templateClosure := customdecode.ExpressionClosureFromVal(args[0])
|
|
varsVal := args[1]
|
|
|
|
// Our historical experience with the hashicorp/template provider's
|
|
// template_file data source tells us that situations where authors
|
|
// must write a string template that generates a string template
|
|
// cause all sorts of confusion, because the same syntax ends up
|
|
// being evaluated in two different contexts with different variables
|
|
// in scope, and new authors tend to be attracted to a function
|
|
// named "template" and so miss that the language has built-in
|
|
// support for inline template expressions.
|
|
//
|
|
// As a compromise to try to meet the (relatively unusual) use-cases
|
|
// where dynamic template fetching is needed without creating an
|
|
// attractive nuisance for those who would be better off just writing
|
|
// a plain inline template, this function imposes constraints on how
|
|
// the template argument may be provided and thus allows us
|
|
// to return slightly more helpful error messages.
|
|
//
|
|
// The only valid way to provide the template argument is as a
|
|
// simple, direct reference to some other value in scope that is
|
|
// of type string:
|
|
// templatestring(local.greeting_template, { name = "Alex" })
|
|
//
|
|
// Those with more unusual desires, such as dynamically generating
|
|
// a template at runtime by trying to concatenate template chunks
|
|
// together, can still do such things by placing the template
|
|
// construction expression in a separate local value and then passing
|
|
// that local value to the template argument. But the restriction is
|
|
// intended to intentionally add an extra "roadbump" so that
|
|
// anyone who mistakenly thinks they need templatestring to render
|
|
// an inline template (a common mistake for new authors with
|
|
// template_file) will hopefully hit this roadblock and refer to
|
|
// the function documentation to learn about the other options that
|
|
// are probably more suitable for what they need.
|
|
switch expr := templateClosure.Expression.(type) {
|
|
case *hclsyntax.TemplateWrapExpr:
|
|
// This situation occurs when someone writes an interpolation-only
|
|
// expression as was required in Terraform v0.11 and earlier.
|
|
// Because older versions of Terraform required this and this
|
|
// habit has been sticky for some authors, we'll return a
|
|
// special error message.
|
|
return cty.UnknownVal(retType), function.NewArgErrorf(
|
|
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
|
|
// this function at all.
|
|
literal := true
|
|
if len(expr.Parts) != 1 {
|
|
literal = false
|
|
} else if _, ok := expr.Parts[0].(*hclsyntax.LiteralValueExpr); !ok {
|
|
literal = false
|
|
}
|
|
if literal {
|
|
return cty.UnknownVal(retType), function.NewArgErrorf(
|
|
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",
|
|
)
|
|
} else {
|
|
return cty.UnknownVal(retType), function.NewArgErrorf(
|
|
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:
|
|
if !isValidTemplateStringExpr(expr) {
|
|
// 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,
|
|
// where we can show a full example.
|
|
return cty.UnknownVal(retType), function.NewArgErrorf(
|
|
0, "invalid template expression: must be a direct reference to a single string from elsewhere, containing valid Terraform template syntax",
|
|
)
|
|
}
|
|
}
|
|
|
|
templateVal, diags := templateClosure.Value()
|
|
if diags.HasErrors() {
|
|
// With the constraints we imposed above the possible errors
|
|
// here are pretty limited: it must be some kind of invalid
|
|
// traversal. As usual HCL diagnostics don't make for very
|
|
// good function errors but we've already filtered out many
|
|
// common reasons for error here, so we should get here pretty
|
|
// rarely.
|
|
return cty.UnknownVal(retType), function.NewArgErrorf(
|
|
0, "invalid template expression: %s",
|
|
diags.Error(),
|
|
)
|
|
}
|
|
if !templateVal.IsKnown() {
|
|
// We'll need to wait until we actually know what the template is.
|
|
return cty.UnknownVal(retType), nil
|
|
}
|
|
if templateVal.Type() != cty.String || templateVal.IsNull() {
|
|
// We're being a little stricter than usual here and requiring
|
|
// exactly a string, rather than just anything that can convert
|
|
// to one. This is because the stringification of a number or
|
|
// boolean value cannot be a useful template (it wouldn't have
|
|
// any template sequences in it) and so far more likely to be
|
|
// a mistake than actually intentional.
|
|
return cty.UnknownVal(retType), function.NewArgErrorf(
|
|
0, "invalid template value: a string is required",
|
|
)
|
|
}
|
|
templateVal, templateMarks := templateVal.Unmark()
|
|
templateStr := templateVal.AsString()
|
|
expr, diags := hclsyntax.ParseTemplate([]byte(templateStr), "<templatestring argument>", hcl.Pos{Line: 1, Column: 1})
|
|
if diags.HasErrors() {
|
|
return cty.UnknownVal(retType), function.NewArgErrorf(
|
|
0, "invalid template: %s",
|
|
diags.Error(),
|
|
)
|
|
}
|
|
|
|
render := makeRenderTemplateFunc(funcsCb, false)
|
|
retVal, err := render(expr, varsVal)
|
|
if err != nil {
|
|
return cty.UnknownVal(retType), err
|
|
}
|
|
retVal, err = convert.Convert(retVal, cty.String)
|
|
if err != nil {
|
|
return cty.UnknownVal(retType), fmt.Errorf("invalid template result: %s", err)
|
|
}
|
|
return retVal.WithMarks(templateMarks), nil
|
|
},
|
|
})
|
|
}
|
|
|
|
func makeRenderTemplateFunc(funcsCb func() (funcs map[string]function.Function, fsFuncs collections.Set[string], templateFuncs collections.Set[string]), allowFS bool) func(expr hcl.Expression, varsVal cty.Value) (cty.Value, error) {
|
|
return 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, fsFuncs, templateFuncs := funcsCb() // this callback indirection is to avoid chicken/egg problems
|
|
funcs := make(map[string]function.Function, len(givenFuncs))
|
|
for name, fn := range givenFuncs {
|
|
plainName := strings.TrimPrefix(name, "core::")
|
|
switch {
|
|
case templateFuncs.Has(plainName):
|
|
funcs[name] = function.New(&function.Spec{
|
|
Params: fn.Params(),
|
|
VarParam: fn.VarParam(),
|
|
Type: func(args []cty.Value) (cty.Type, error) {
|
|
return cty.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) {
|
|
return cty.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)
|
|
if diags.HasErrors() {
|
|
return cty.DynamicVal, diags
|
|
}
|
|
return val, nil
|
|
}
|
|
}
|
|
|
|
func isValidTemplateStringExpr(expr hcl.Expression) bool {
|
|
// Our goal with this heuristic is to be as permissive as possible with
|
|
// allowing things that authors might try to use as references to a
|
|
// template string defined elsewhere, while rejecting complex expressions
|
|
// that seem like they might be trying to construct templates dynamically
|
|
// or might have resulted from a misunderstanding that "templatestring" is
|
|
// the only way to render a template, because someone hasn't learned
|
|
// about template expressions yet.
|
|
//
|
|
// This is here only to give better feedback to folks who seem to be using
|
|
// templatestring for something other than what it's intended for, and not
|
|
// to block dynamic template generation altogether. Authors who have a
|
|
// genuine need for dynamic template generation can always assert that to
|
|
// Terraform by factoring out their dynamic generation into a local value
|
|
// and referring to it; this rule is just a little speedbump to prompt
|
|
// the author to consider whether there's a better way to solve their
|
|
// problem, as opposed to just using the first solution they found.
|
|
switch expr := expr.(type) {
|
|
case *hclsyntax.ScopeTraversalExpr:
|
|
// A simple static reference from the current scope is always valid.
|
|
return true
|
|
|
|
case *hclsyntax.RelativeTraversalExpr:
|
|
// Relative traversals are allowed as long as they begin from
|
|
// something that would otherwise be allowed.
|
|
return isValidTemplateStringExpr(expr.Source)
|
|
|
|
case *hclsyntax.IndexExpr:
|
|
// Index expressions are allowed as long as the collection is
|
|
// also specified using an expression that conforms to these rules.
|
|
// The key operand is intentionally unconstrained because that
|
|
// is a rule for how to select an element, and so doesn't represent
|
|
// a source from which the template string is being retrieved.
|
|
return isValidTemplateStringExpr(expr.Collection)
|
|
|
|
case *hclsyntax.SplatExpr:
|
|
// Splat expressions would be weird to use because they'd typically
|
|
// return a tuple and that wouldn't be valid as a template string,
|
|
// but we allow it here (as long as the operand would otherwise have
|
|
// been allowed) because then we'll let the type mismatch error
|
|
// show through, and that's likely a more helpful error message.
|
|
return isValidTemplateStringExpr(expr.Source)
|
|
|
|
default:
|
|
// Nothing else is allowed.
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Replace searches a given string for another given substring,
|
|
// and replaces all occurences with a given replacement string.
|
|
func Replace(str, substr, replace cty.Value) (cty.Value, error) {
|
|
return ReplaceFunc.Call([]cty.Value{str, substr, replace})
|
|
}
|
|
|
|
func StrContains(str, substr cty.Value) (cty.Value, error) {
|
|
return StrContainsFunc.Call([]cty.Value{str, substr})
|
|
}
|