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  }