github.com/terraform-linters/tflint@v0.51.2-0.20240520175844-3750771571b6/terraform/lang/eval.go (about) 1 package lang 2 3 import ( 4 "fmt" 5 6 "github.com/hashicorp/hcl/v2" 7 "github.com/terraform-linters/tflint-plugin-sdk/hclext" 8 "github.com/terraform-linters/tflint/terraform/addrs" 9 "github.com/terraform-linters/tflint/terraform/tfdiags" 10 "github.com/terraform-linters/tflint/terraform/tfhcl" 11 "github.com/zclconf/go-cty/cty" 12 "github.com/zclconf/go-cty/cty/convert" 13 ) 14 15 // ExpandBlock expands "dynamic" blocks and resources/modules with count/for_each. 16 // Note that Terraform only expands dynamic blocks, but TFLint also expands 17 // count/for_each here. 18 // 19 // Expressions in expanded blocks are evaluated immediately, so all variables and 20 // function calls contained in attributes specified in the body schema are gathered. 21 func (s *Scope) ExpandBlock(body hcl.Body, schema *hclext.BodySchema) (hcl.Body, hcl.Diagnostics) { 22 traversals := tfhcl.ExpandVariablesHCLExt(body, schema) 23 refs, diags := References(traversals) 24 25 exprs := tfhcl.ExpandExpressionsHCLExt(body, schema) 26 funcCalls := []*FunctionCall{} 27 for _, expr := range exprs { 28 calls, funcDiags := FunctionCallsInExpr(expr) 29 diags = diags.Extend(funcDiags) 30 funcCalls = append(funcCalls, calls...) 31 } 32 33 ctx, ctxDiags := s.EvalContext(refs, funcCalls) 34 diags = diags.Extend(ctxDiags) 35 36 return tfhcl.Expand(body, ctx), diags 37 } 38 39 // EvalExpr evaluates a single expression in the receiving context and returns 40 // the resulting value. The value will be converted to the given type before 41 // it is returned if possible, or else an error diagnostic will be produced 42 // describing the conversion error. 43 // 44 // Pass an expected type of cty.DynamicPseudoType to skip automatic conversion 45 // and just obtain the returned value directly. 46 // 47 // If the returned diagnostics contains errors then the result may be 48 // incomplete, but will always be of the requested type. 49 func (s *Scope) EvalExpr(expr hcl.Expression, wantType cty.Type) (cty.Value, hcl.Diagnostics) { 50 refs, diags := ReferencesInExpr(expr) 51 funcCalls, funcDiags := FunctionCallsInExpr(expr) 52 diags = diags.Extend(funcDiags) 53 54 ctx, ctxDiags := s.EvalContext(refs, funcCalls) 55 diags = diags.Extend(ctxDiags) 56 if diags.HasErrors() { 57 // We'll stop early if we found problems in the references, because 58 // it's likely evaluation will produce redundant copies of the same errors. 59 return cty.UnknownVal(wantType), diags 60 } 61 62 val, evalDiags := expr.Value(ctx) 63 diags = diags.Extend(evalDiags) 64 65 if wantType != cty.DynamicPseudoType { 66 var convErr error 67 val, convErr = convert.Convert(val, wantType) 68 if convErr != nil { 69 val = cty.UnknownVal(wantType) 70 diags = diags.Append(&hcl.Diagnostic{ 71 Severity: hcl.DiagError, 72 Summary: "Incorrect value type", 73 Detail: fmt.Sprintf("Invalid expression value: %s.", tfdiags.FormatError(convErr)), 74 Subject: expr.Range().Ptr(), 75 Expression: expr, 76 EvalContext: ctx, 77 }) 78 } 79 } 80 81 return val, diags 82 } 83 84 // EvalContext constructs a HCL expression evaluation context whose variable 85 // scope contains sufficient values to satisfy the given set of references 86 // and function calls. 87 // 88 // Most callers should prefer to use the evaluation helper methods that 89 // this type offers, but this is here for less common situations where the 90 // caller will handle the evaluation calls itself. 91 func (s *Scope) EvalContext(refs []*addrs.Reference, funcCalls []*FunctionCall) (*hcl.EvalContext, hcl.Diagnostics) { 92 return s.evalContext(refs, s.SelfAddr, funcCalls) 93 } 94 95 func (s *Scope) evalContext(refs []*addrs.Reference, selfAddr addrs.Referenceable, funcCalls []*FunctionCall) (*hcl.EvalContext, hcl.Diagnostics) { 96 if s == nil { 97 panic("attempt to construct EvalContext for nil Scope") 98 } 99 100 var diags hcl.Diagnostics 101 vals := make(map[string]cty.Value) 102 funcs := s.Functions() 103 // Provider-defined functions introduced in Terraform v1.8 cannot be 104 // evaluated statically in many cases. Here, we avoid the error by dynamically 105 // generating an evaluation context in which the provider-defined functions 106 // in the given expression are replaced with mock functions. 107 for _, call := range funcCalls { 108 if !call.IsProviderDefined() { 109 continue 110 } 111 // Some provider-defined functions are supported, 112 // so only generate mocks for undefined functions 113 if _, exists := funcs[call.Name]; !exists { 114 funcs[call.Name] = NewMockFunction(call) 115 } 116 } 117 ctx := &hcl.EvalContext{ 118 Variables: vals, 119 Functions: funcs, 120 } 121 122 if len(refs) == 0 { 123 // Easy path for common case where there are no references at all. 124 return ctx, diags 125 } 126 127 // The reference set we are given has not been de-duped, and so there can 128 // be redundant requests in it for two reasons: 129 // - The same item is referenced multiple times 130 // - Both an item and that item's container are separately referenced. 131 // We will still visit every reference here and ask our data source for 132 // it, since that allows us to gather a full set of any errors and 133 // warnings, but once we've gathered all the data we'll then skip anything 134 // that's redundant in the process of populating our values map. 135 managedResources := map[string]cty.Value{} 136 inputVariables := map[string]cty.Value{} 137 localValues := map[string]cty.Value{} 138 pathAttrs := map[string]cty.Value{} 139 terraformAttrs := map[string]cty.Value{} 140 countAttrs := map[string]cty.Value{} 141 forEachAttrs := map[string]cty.Value{} 142 143 for _, ref := range refs { 144 rng := ref.SourceRange 145 146 rawSubj := ref.Subject 147 148 // This type switch must cover all of the "Referenceable" implementations 149 // in package addrs, however we are removing the possibility of 150 // Instances beforehand. 151 switch addr := rawSubj.(type) { 152 case addrs.ResourceInstance: 153 rawSubj = addr.ContainingResource() 154 } 155 156 switch subj := rawSubj.(type) { 157 case addrs.Resource: 158 // Managed resources are not supported by TFLint, but it does support arbitrary 159 // key names, so it gathers the referenced resource names. 160 if subj.Mode != addrs.ManagedResourceMode { 161 continue 162 } 163 managedResources[subj.Type] = cty.UnknownVal(cty.DynamicPseudoType) 164 165 case addrs.InputVariable: 166 val, valDiags := normalizeRefValue(s.Data.GetInputVariable(subj, rng)) 167 diags = diags.Extend(valDiags) 168 inputVariables[subj.Name] = val 169 170 case addrs.LocalValue: 171 val, valDiags := normalizeRefValue(s.Data.GetLocalValue(subj, rng)) 172 diags = diags.Extend(valDiags) 173 localValues[subj.Name] = val 174 175 case addrs.PathAttr: 176 val, valDiags := normalizeRefValue(s.Data.GetPathAttr(subj, rng)) 177 diags = diags.Extend(valDiags) 178 pathAttrs[subj.Name] = val 179 180 case addrs.TerraformAttr: 181 val, valDiags := normalizeRefValue(s.Data.GetTerraformAttr(subj, rng)) 182 diags = diags.Extend(valDiags) 183 terraformAttrs[subj.Name] = val 184 185 case addrs.CountAttr: 186 val, valDiags := normalizeRefValue(s.Data.GetCountAttr(subj, rng)) 187 diags = diags.Extend(valDiags) 188 countAttrs[subj.Name] = val 189 190 case addrs.ForEachAttr: 191 val, valDiags := normalizeRefValue(s.Data.GetForEachAttr(subj, rng)) 192 diags = diags.Extend(valDiags) 193 forEachAttrs[subj.Name] = val 194 } 195 } 196 197 // Managed resources are exposed in two different locations. This is 198 // at the top level where the resource type name is the root of the 199 // traversal. 200 for k, v := range managedResources { 201 vals[k] = v 202 } 203 204 vals["var"] = cty.ObjectVal(inputVariables) 205 vals["local"] = cty.ObjectVal(localValues) 206 vals["path"] = cty.ObjectVal(pathAttrs) 207 vals["terraform"] = cty.ObjectVal(terraformAttrs) 208 vals["count"] = cty.ObjectVal(countAttrs) 209 vals["each"] = cty.ObjectVal(forEachAttrs) 210 211 // The following are unknown values as they are not supported by TFLint. 212 vals["resource"] = cty.UnknownVal(cty.DynamicPseudoType) 213 vals["data"] = cty.UnknownVal(cty.DynamicPseudoType) 214 vals["module"] = cty.UnknownVal(cty.DynamicPseudoType) 215 vals["self"] = cty.UnknownVal(cty.DynamicPseudoType) 216 217 return ctx, diags 218 } 219 220 func normalizeRefValue(val cty.Value, diags hcl.Diagnostics) (cty.Value, hcl.Diagnostics) { 221 if diags.HasErrors() { 222 // If there are errors then we will force an unknown result so that 223 // we can still evaluate and catch type errors but we'll avoid 224 // producing redundant re-statements of the same errors we've already 225 // dealt with here. 226 return cty.UnknownVal(val.Type()), diags 227 } 228 return val, diags 229 }