| // Copyright IBM Corp. 2014, 2026 | |
| // 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 | |
| } | |
| if val.IsNull() { | |
| return cty.DynamicVal, &hcl.Diagnostic{ | |
| Severity: hcl.DiagError, | |
| Summary: "Template result is null", | |
| Detail: "The result of the template is null, which is not a valid result for a templatestring call.", | |
| Subject: expr.Range().Ptr(), | |
| } | |
| } | |
| 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}) | |
| } |