github.com/opentofu/opentofu@v1.7.1/internal/lang/funcs/render_template.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package funcs
     7  
     8  import (
     9  	"fmt"
    10  
    11  	"github.com/hashicorp/hcl/v2"
    12  	"github.com/hashicorp/hcl/v2/hclsyntax"
    13  	"github.com/zclconf/go-cty/cty"
    14  	"github.com/zclconf/go-cty/cty/function"
    15  )
    16  
    17  func renderTemplate(expr hcl.Expression, varsVal cty.Value, funcs map[string]function.Function) (cty.Value, error) {
    18  	if varsTy := varsVal.Type(); !(varsTy.IsMapType() || varsTy.IsObjectType()) {
    19  		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
    20  	}
    21  
    22  	ctx := &hcl.EvalContext{
    23  		Variables: varsVal.AsValueMap(),
    24  		Functions: funcs,
    25  	}
    26  
    27  	// We require all of the variables to be valid HCL identifiers, because
    28  	// otherwise there would be no way to refer to them in the template
    29  	// anyway. Rejecting this here gives better feedback to the user
    30  	// than a syntax error somewhere in the template itself.
    31  	for n := range ctx.Variables {
    32  		if !hclsyntax.ValidIdentifier(n) {
    33  			// This error message intentionally doesn't describe _all_ of
    34  			// the different permutations that are technically valid as an
    35  			// HCL identifier, but rather focuses on what we might
    36  			// consider to be an "idiomatic" variable name.
    37  			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)
    38  		}
    39  	}
    40  
    41  	// currFilename stores the filename of the template file, if any.
    42  	currFilename := expr.Range().Filename
    43  
    44  	// We'll pre-check references in the template here so we can give a
    45  	// more specialized error message than HCL would by default, so it's
    46  	// clearer that this problem is coming from a templatefile/templatestring call.
    47  	for _, traversal := range expr.Variables() {
    48  		root := traversal.RootName()
    49  		referencedPos := fmt.Sprintf("%q", root)
    50  		if currFilename != templateStringFilename {
    51  			referencedPos = fmt.Sprintf("%q, referenced at %s", root, traversal[0].SourceRange())
    52  		}
    53  		if _, ok := ctx.Variables[root]; !ok {
    54  			return cty.DynamicVal, function.NewArgErrorf(1, "vars map does not contain key %s", referencedPos)
    55  		}
    56  	}
    57  
    58  	val, diags := expr.Value(ctx)
    59  	if diags.HasErrors() {
    60  		for _, diag := range diags {
    61  			// Roll up recursive errors
    62  			if extra, ok := diag.Extra.(hclsyntax.FunctionCallDiagExtra); ok {
    63  				if extra.CalledFunctionName() == "templatefile" {
    64  					err := extra.FunctionCallError()
    65  					if err, ok := err.(ErrorTemplateRecursionLimit); ok {
    66  						return cty.DynamicVal, ErrorTemplateRecursionLimit{sources: append(err.sources, diag.Subject.String())}
    67  					}
    68  				}
    69  			}
    70  		}
    71  		return cty.DynamicVal, diags
    72  	}
    73  
    74  	return val, nil
    75  }