github.com/jpreese/tflint@v0.19.2-0.20200908152133-b01686250fb6/tflint/runner_eval.go (about) 1 package tflint 2 3 import ( 4 "fmt" 5 "log" 6 7 hcl "github.com/hashicorp/hcl/v2" 8 "github.com/hashicorp/terraform/addrs" 9 "github.com/hashicorp/terraform/configs" 10 "github.com/hashicorp/terraform/configs/configschema" 11 "github.com/hashicorp/terraform/lang" 12 "github.com/hashicorp/terraform/terraform" 13 "github.com/zclconf/go-cty/cty" 14 "github.com/zclconf/go-cty/cty/convert" 15 "github.com/zclconf/go-cty/cty/gocty" 16 ) 17 18 // EvaluateExpr evaluates the expression and reflects the result in the value of `ret`. 19 // In the future, it will be no longer needed because all evaluation requests are invoked from RPC client 20 func (r *Runner) EvaluateExpr(expr hcl.Expression, ret interface{}) error { 21 val, err := r.EvalExpr(expr, ret, cty.Type{}) 22 if err != nil { 23 return err 24 } 25 return r.fromCtyValue(val, expr, ret) 26 } 27 28 // EvaluateExprType is like EvaluateExpr, but also accepts a known cty.Type to pass to EvalExpr 29 func (r *Runner) EvaluateExprType(expr hcl.Expression, ret interface{}, wantType cty.Type) error { 30 val, err := r.EvalExpr(expr, ret, wantType) 31 if err != nil { 32 return err 33 } 34 return r.fromCtyValue(val, expr, ret) 35 } 36 37 // EvalExpr is a wrapper of terraform.BultinEvalContext.EvaluateExpr 38 // In addition, this method determines whether the expression is evaluable, contains no unknown values, and so on. 39 // The returned cty.Value is converted according to the value passed as `ret`. 40 func (r *Runner) EvalExpr(expr hcl.Expression, ret interface{}, wantType cty.Type) (cty.Value, error) { 41 evaluable, err := isEvaluableExpr(expr) 42 if err != nil { 43 err := &Error{ 44 Code: EvaluationError, 45 Level: ErrorLevel, 46 Message: fmt.Sprintf( 47 "Failed to parse an expression in %s:%d", 48 expr.Range().Filename, 49 expr.Range().Start.Line, 50 ), 51 Cause: err, 52 } 53 log.Printf("[ERROR] %s", err) 54 return cty.NullVal(cty.NilType), err 55 } 56 57 if !evaluable { 58 err := &Error{ 59 Code: UnevaluableError, 60 Level: WarningLevel, 61 Message: fmt.Sprintf( 62 "Unevaluable expression found in %s:%d", 63 expr.Range().Filename, 64 expr.Range().Start.Line, 65 ), 66 } 67 log.Printf("[WARN] %s; TFLint ignores an unevaluable expression.", err) 68 return cty.NullVal(cty.NilType), err 69 } 70 71 if wantType == (cty.Type{}) { 72 switch ret.(type) { 73 case *string, string: 74 wantType = cty.String 75 case *int, int: 76 wantType = cty.Number 77 case *[]string, []string: 78 wantType = cty.List(cty.String) 79 case *[]int, []int: 80 wantType = cty.List(cty.Number) 81 case *map[string]string, map[string]string: 82 wantType = cty.Map(cty.String) 83 case *map[string]int, map[string]int: 84 wantType = cty.Map(cty.Number) 85 default: 86 panic(fmt.Errorf("Unexpected result type: %T", ret)) 87 } 88 } 89 90 val, diags := r.ctx.EvaluateExpr(expr, wantType, nil) 91 if diags.HasErrors() { 92 err := &Error{ 93 Code: EvaluationError, 94 Level: ErrorLevel, 95 Message: fmt.Sprintf( 96 "Failed to eval an expression in %s:%d", 97 expr.Range().Filename, 98 expr.Range().Start.Line, 99 ), 100 Cause: diags.Err(), 101 } 102 log.Printf("[ERROR] %s", err) 103 return cty.NullVal(cty.NilType), err 104 } 105 106 err = cty.Walk(val, func(path cty.Path, v cty.Value) (bool, error) { 107 if !v.IsKnown() { 108 err := &Error{ 109 Code: UnknownValueError, 110 Level: WarningLevel, 111 Message: fmt.Sprintf( 112 "Unknown value found in %s:%d; Please use environment variables or tfvars to set the value", 113 expr.Range().Filename, 114 expr.Range().Start.Line, 115 ), 116 } 117 log.Printf("[WARN] %s; TFLint ignores an expression includes an unknown value.", err) 118 return false, err 119 } 120 121 if v.IsNull() { 122 err := &Error{ 123 Code: NullValueError, 124 Level: WarningLevel, 125 Message: fmt.Sprintf( 126 "Null value found in %s:%d", 127 expr.Range().Filename, 128 expr.Range().Start.Line, 129 ), 130 } 131 log.Printf("[WARN] %s; TFLint ignores an expression includes an null value.", err) 132 return false, err 133 } 134 135 return true, nil 136 }) 137 138 if err != nil { 139 return cty.NullVal(cty.NilType), err 140 } 141 142 return val, nil 143 } 144 145 // EvaluateBlock is a wrapper of terraform.BultinEvalContext.EvaluateBlock and gocty.FromCtyValue 146 func (r *Runner) EvaluateBlock(block *hcl.Block, schema *configschema.Block, ret interface{}) error { 147 evaluable, err := isEvaluableBlock(block.Body, schema) 148 if err != nil { 149 err := &Error{ 150 Code: EvaluationError, 151 Level: ErrorLevel, 152 Message: fmt.Sprintf( 153 "Failed to parse a block in %s:%d", 154 block.DefRange.Filename, 155 block.DefRange.Start.Line, 156 ), 157 Cause: err, 158 } 159 log.Printf("[ERROR] %s", err) 160 return err 161 } 162 163 if !evaluable { 164 err := &Error{ 165 Code: UnevaluableError, 166 Level: WarningLevel, 167 Message: fmt.Sprintf( 168 "Unevaluable block found in %s:%d", 169 block.DefRange.Filename, 170 block.DefRange.Start.Line, 171 ), 172 } 173 log.Printf("[WARN] %s; TFLint ignores an unevaluable block.", err) 174 return err 175 } 176 177 val, _, diags := r.ctx.EvaluateBlock(block.Body, schema, nil, terraform.EvalDataForNoInstanceKey) 178 if diags.HasErrors() { 179 err := &Error{ 180 Code: EvaluationError, 181 Level: ErrorLevel, 182 Message: fmt.Sprintf( 183 "Failed to eval a block in %s:%d", 184 block.DefRange.Filename, 185 block.DefRange.Start.Line, 186 ), 187 Cause: diags.Err(), 188 } 189 log.Printf("[ERROR] %s", err) 190 return err 191 } 192 193 err = cty.Walk(val, func(path cty.Path, v cty.Value) (bool, error) { 194 if !v.IsKnown() { 195 err := &Error{ 196 Code: UnknownValueError, 197 Level: WarningLevel, 198 Message: fmt.Sprintf( 199 "Unknown value found in %s:%d; Please use environment variables or tfvars to set the value", 200 block.DefRange.Filename, 201 block.DefRange.Start.Line, 202 ), 203 } 204 log.Printf("[WARN] %s; TFLint ignores a block includes an unknown value.", err) 205 return false, err 206 } 207 208 return true, nil 209 }) 210 if err != nil { 211 return err 212 } 213 214 val, err = cty.Transform(val, func(path cty.Path, v cty.Value) (cty.Value, error) { 215 if v.IsNull() { 216 log.Printf( 217 "[DEBUG] Null value found in %s:%d, but TFLint treats this value as an empty value", 218 block.DefRange.Filename, 219 block.DefRange.Start.Line, 220 ) 221 return cty.StringVal(""), nil 222 } 223 return v, nil 224 }) 225 if err != nil { 226 return err 227 } 228 229 switch ret.(type) { 230 case *map[string]string: 231 val, err = convert.Convert(val, cty.Map(cty.String)) 232 case *map[string]int: 233 val, err = convert.Convert(val, cty.Map(cty.Number)) 234 } 235 236 if err != nil { 237 err := &Error{ 238 Code: TypeConversionError, 239 Level: ErrorLevel, 240 Message: fmt.Sprintf( 241 "Invalid type block in %s:%d", 242 block.DefRange.Filename, 243 block.DefRange.Start.Line, 244 ), 245 Cause: err, 246 } 247 log.Printf("[ERROR] %s", err) 248 return err 249 } 250 251 err = gocty.FromCtyValue(val, ret) 252 if err != nil { 253 err := &Error{ 254 Code: TypeMismatchError, 255 Level: ErrorLevel, 256 Message: fmt.Sprintf( 257 "Invalid type block in %s:%d", 258 block.DefRange.Filename, 259 block.DefRange.Start.Line, 260 ), 261 Cause: err, 262 } 263 log.Printf("[ERROR] %s", err) 264 return err 265 } 266 return nil 267 } 268 269 func (r *Runner) fromCtyValue(val cty.Value, expr hcl.Expression, ret interface{}) error { 270 err := gocty.FromCtyValue(val, ret) 271 if err != nil { 272 err := &Error{ 273 Code: TypeMismatchError, 274 Level: ErrorLevel, 275 Message: fmt.Sprintf( 276 "Invalid type expression in %s:%d", 277 expr.Range().Filename, 278 expr.Range().Start.Line, 279 ), 280 Cause: err, 281 } 282 log.Printf("[ERROR] %s", err) 283 return err 284 } 285 return nil 286 } 287 288 func isEvaluableExpr(expr hcl.Expression) (bool, error) { 289 refs, diags := lang.ReferencesInExpr(expr) 290 if diags.HasErrors() { 291 return false, diags.Err() 292 } 293 for _, ref := range refs { 294 if !isEvaluableRef(ref) { 295 return false, nil 296 } 297 } 298 return true, nil 299 } 300 301 func isEvaluableBlock(body hcl.Body, schema *configschema.Block) (bool, error) { 302 refs, diags := lang.ReferencesInBlock(body, schema) 303 if diags.HasErrors() { 304 return false, diags.Err() 305 } 306 for _, ref := range refs { 307 if !isEvaluableRef(ref) { 308 return false, nil 309 } 310 } 311 return true, nil 312 } 313 314 func isEvaluableRef(ref *addrs.Reference) bool { 315 switch ref.Subject.(type) { 316 case addrs.InputVariable: 317 return true 318 case addrs.TerraformAttr: 319 return true 320 case addrs.PathAttr: 321 return true 322 default: 323 return false 324 } 325 } 326 327 // willEvaluateResource checks whether the passed resource will be evaluated. 328 // If `count` is 0 or `for_each` is empty, Terraform will not evaluate the attributes of that resource. 329 func (r *Runner) willEvaluateResource(resource *configs.Resource) (bool, error) { 330 var err error 331 if resource.Count != nil { 332 count := 1 333 err = r.EvaluateExpr(resource.Count, &count) 334 if err == nil && count == 0 { 335 return false, nil 336 } 337 } else if resource.ForEach != nil { 338 var forEach cty.Value 339 forEach, err = r.EvalExpr(resource.ForEach, nil, cty.DynamicPseudoType) 340 if err == nil { 341 if !forEach.CanIterateElements() { 342 return false, fmt.Errorf("The `for_each` value is not iterable in %s:%d", resource.ForEach.Range().Filename, resource.ForEach.Range().Start.Line) 343 } 344 if forEach.LengthInt() == 0 { 345 return false, nil 346 } 347 } 348 } 349 350 if err == nil { 351 return true, nil 352 } 353 if appErr, ok := err.(*Error); ok { 354 switch appErr.Level { 355 case WarningLevel: 356 return false, nil 357 case ErrorLevel: 358 return false, err 359 default: 360 panic(appErr) 361 } 362 } else { 363 return false, err 364 } 365 }