Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 60 additions & 12 deletions internal/lang/funcs/string.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,8 +213,6 @@ func MakeTemplateStringFunc(funcsCb func() (funcs map[string]function.Function,
// 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.ScopeTraversalExpr:
// A standalone traversal is always acceptable.
case *hclsyntax.TemplateWrapExpr:
// This situation occurs when someone writes an interpolation-only
// expression as was required in Terraform v0.11 and earlier.
Expand Down Expand Up @@ -249,16 +247,17 @@ func MakeTemplateStringFunc(funcsCb func() (funcs map[string]function.Function,
)
}
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,
// 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",
)
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()
Expand Down Expand Up @@ -385,6 +384,55 @@ func makeRenderTemplateFunc(funcsCb func() (funcs map[string]function.Function,
}
}

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) {
Expand Down
49 changes: 49 additions & 0 deletions internal/lang/funcs/string_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,55 @@ func TestTemplateString(t *testing.T) {
cty.StringVal(`it's a value`),
``,
},
{
`data.whatever.whatever[each.key].result`,
map[string]cty.Value{
"data": cty.ObjectVal(map[string]cty.Value{
"whatever": cty.ObjectVal(map[string]cty.Value{
"whatever": cty.MapVal(map[string]cty.Value{
"foo": cty.ObjectVal(map[string]cty.Value{
"result": cty.StringVal("it's ${a}"),
}),
}),
}),
}),
"each": cty.ObjectVal(map[string]cty.Value{
"key": cty.StringVal("foo"),
}),
},
cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("a value"),
}),
cty.StringVal(`it's a value`),
``,
},
{
`data.whatever.whatever[*].result`,
map[string]cty.Value{
"data": cty.ObjectVal(map[string]cty.Value{
"whatever": cty.ObjectVal(map[string]cty.Value{
"whatever": cty.TupleVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"result": cty.StringVal("it's ${a}"),
}),
}),
}),
}),
"each": cty.ObjectVal(map[string]cty.Value{
"key": cty.StringVal("foo"),
}),
},
cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("a value"),
}),
cty.NilVal,
// We have an intentional hole in our heuristic for whether the
// first argument is a suitable expression which permits splat
// expressions just so that we can return the type mismatch error
// from the result not being a string, instead of the more general
// error about it not being a supported expression type.
`invalid template value: a string is required`,
},
{
`"can't write $${not_allowed}"`,
map[string]cty.Value{},
Expand Down
29 changes: 17 additions & 12 deletions website/docs/language/functions/templatestring.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ description: |-

-> **Note:** The `templatestring` function is intended for advanced use cases. Most use cases require only a [string template expression](/terraform/language/expressions/strings#string-templates). To render a template from a file, use the [`templatefile` function](/terraform/language/functions/templatefile).

`templatestring` renders a template using a supplied set of template variables.
`templatestring` renders a template given as a string value, using a supplied set of template variables.

```hcl
templatefile(ref, vars)
templatestring(ref, vars)
```

The first parameter must be a simple reference to string value containing the template: for example, `data.aws_s3_object.example.body` or `local.inline_template`.
The first parameter must be a direct reference to string value containing the template: for example, `data.aws_s3_object.example.body` or `local.inline_template`.

It is **not** valid to supply the template expression directly as the first argument:

Expand All @@ -25,23 +25,18 @@ templatestring("Hello, $${name}", {
})
```

Instead of the above, you should instead use a string template expression:
Instead of the above, you should use a normal string template expression:

```hcl
"Hello, ${var.name}"
```

The `templatestring` function is needed only when the template is available as a named object in the current module.
The `templatestring` function is needed only when the template is available from a named object, such as a data resource, declared in the current module.

The template syntax is the same as for
[string templates](/terraform/language/expressions/strings#string-templates)
in the main Terraform language, including interpolation sequences delimited with
`${` ... `}`.

Strings in the Terraform language are sequences of Unicode characters, so
this function will interpret the file contents as UTF-8 encoded text and
return the resulting Unicode characters. If the template contains invalid UTF-8
sequences then this function will produce an error.
`${` ... `}`.

## Example

Expand All @@ -60,7 +55,17 @@ output "example" {
}
```

For more examples of how to use templates, please see the documentation for the [`templatefile`](/terraform/language/functions/templatefile#Examples) function.
For more examples of how to use templates, refer to the documentation for [the `templatefile` function](/terraform/language/functions/templatefile#Examples).

## Dynamic Template Construction

This function is primarily intended for rendering templates fetched as a single string from remote locations, often using data resources.

You can also use this as a way to construct a template dynamically and then render it, but we recommend treating that only as a last resort because the result tends to be hard to understand, hard to maintain, and fragile to unexpected input.

The restrictions on what kind of syntax is allowed in the first argument are a guardrail to help avoid those new to Terraform thinking that this function is the primary way to render templates in Terraform, but you can bypass those restrictions if you wish by writing an expression that builds a template dynamically and then assigning that expression to a [local value](/terraform/language/values/locals). You can then use a reference to that local value as the first argument to `templatestring`.

If you _do_ choose to construct templates from parts dynamically, be mindful that Terraform has built-in functions that can interact with the local filesystem, and so maliciously-crafted input might produce a template whose result includes data from arbitrary files on the system where Terraform is running.

## Related Functions

Expand Down