github.com/hashicorp/packer@v1.14.3/hcl2template/function/templatefile.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: BUSL-1.1 3 4 package function 5 6 import ( 7 "fmt" 8 9 "github.com/hashicorp/go-cty-funcs/filesystem" 10 "github.com/hashicorp/hcl/v2" 11 "github.com/hashicorp/hcl/v2/hclsyntax" 12 "github.com/zclconf/go-cty/cty" 13 "github.com/zclconf/go-cty/cty/function" 14 ) 15 16 // MakeTemplateFileFunc constructs a function that takes a file path and 17 // an arbitrary object of named values and attempts to render the referenced 18 // file as a template using HCL template syntax. 19 // 20 // The template itself may recursively call other functions so a callback 21 // must be provided to get access to those functions. The template cannot, 22 // however, access any variables defined in the scope: it is restricted only to 23 // those variables provided in the second function argument. 24 // 25 // As a special exception, a referenced template file may not recursively call 26 // the templatefile function, since that would risk the same file being 27 // included into itself indefinitely. 28 func MakeTemplateFileFunc(baseDir string, funcsCb func() map[string]function.Function) function.Function { 29 30 params := []function.Parameter{ 31 { 32 Name: "path", 33 Type: cty.String, 34 }, 35 { 36 Name: "vars", 37 Type: cty.DynamicPseudoType, 38 }, 39 } 40 41 loadTmpl := func(fn string) (hcl.Expression, error) { 42 // We re-use File here to ensure the same filename interpretation 43 // as it does, along with its other safety checks. 44 tmplVal, err := filesystem.File(baseDir, cty.StringVal(fn)) 45 if err != nil { 46 return nil, err 47 } 48 49 expr, diags := hclsyntax.ParseTemplate([]byte(tmplVal.AsString()), fn, hcl.Pos{Line: 1, Column: 1}) 50 if diags.HasErrors() { 51 return nil, diags 52 } 53 54 return expr, nil 55 } 56 57 renderTmpl := func(expr hcl.Expression, varsVal cty.Value) (cty.Value, error) { 58 if varsTy := varsVal.Type(); !(varsTy.IsMapType() || varsTy.IsObjectType()) { 59 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 60 } 61 62 ctx := &hcl.EvalContext{ 63 Variables: varsVal.AsValueMap(), 64 } 65 66 // We require all of the variables to be valid HCL identifiers, because 67 // otherwise there would be no way to refer to them in the template 68 // anyway. Rejecting this here gives better feedback to the user 69 // than a syntax error somewhere in the template itself. 70 for n := range ctx.Variables { 71 if !hclsyntax.ValidIdentifier(n) { 72 // This error message intentionally doesn't describe _all_ of 73 // the different permutations that are technically valid as an 74 // HCL identifier, but rather focuses on what we might 75 // consider to be an "idiomatic" variable name. 76 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) 77 } 78 } 79 80 // We'll pre-check references in the template here so we can give a 81 // more specialized error message than HCL would by default, so it's 82 // clearer that this problem is coming from a templatefile call. 83 for _, traversal := range expr.Variables() { 84 root := traversal.RootName() 85 if _, ok := ctx.Variables[root]; !ok { 86 return cty.DynamicVal, function.NewArgErrorf(1, "vars map does not contain key %q, referenced at %s", root, traversal[0].SourceRange()) 87 } 88 } 89 90 givenFuncs := funcsCb() // this callback indirection is to avoid chicken/egg problems 91 funcs := make(map[string]function.Function, len(givenFuncs)) 92 for name, fn := range givenFuncs { 93 if name == "templatefile" { 94 // We stub this one out to prevent recursive calls. 95 funcs[name] = function.New(&function.Spec{ 96 Params: params, 97 Type: func(args []cty.Value) (cty.Type, error) { 98 return cty.NilType, fmt.Errorf("cannot recursively call templatefile from inside templatefile call") 99 }, 100 }) 101 continue 102 } 103 funcs[name] = fn 104 } 105 ctx.Functions = funcs 106 107 val, diags := expr.Value(ctx) 108 if diags.HasErrors() { 109 return cty.DynamicVal, diags 110 } 111 return val, nil 112 } 113 114 return function.New(&function.Spec{ 115 Params: params, 116 Type: func(args []cty.Value) (cty.Type, error) { 117 if !(args[0].IsKnown() && args[1].IsKnown()) { 118 return cty.DynamicPseudoType, nil 119 } 120 121 // We'll render our template now to see what result type it 122 // produces. A template consisting only of a single interpolation 123 // can potentially return any type. 124 expr, err := loadTmpl(args[0].AsString()) 125 if err != nil { 126 return cty.DynamicPseudoType, err 127 } 128 129 // This is safe even if args[1] contains unknowns because the HCL 130 // template renderer itself knows how to short-circuit those. 131 val, err := renderTmpl(expr, args[1]) 132 return val.Type(), err 133 }, 134 Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 135 expr, err := loadTmpl(args[0].AsString()) 136 if err != nil { 137 return cty.DynamicVal, err 138 } 139 return renderTmpl(expr, args[1]) 140 }, 141 }) 142 143 }