github.com/gkze/tflint@v0.8.0/tflint/runner.go (about)

     1  package tflint
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"log"
     7  	"path/filepath"
     8  	"strings"
     9  	"sync"
    10  
    11  	"github.com/hashicorp/hcl2/hcl"
    12  	"github.com/hashicorp/terraform/addrs"
    13  	"github.com/hashicorp/terraform/configs"
    14  	"github.com/hashicorp/terraform/lang"
    15  	"github.com/hashicorp/terraform/terraform"
    16  	"github.com/wata727/tflint/client"
    17  	"github.com/wata727/tflint/issue"
    18  	"github.com/zclconf/go-cty/cty"
    19  	"github.com/zclconf/go-cty/cty/convert"
    20  	"github.com/zclconf/go-cty/cty/gocty"
    21  )
    22  
    23  // Runner checks templates according rules.
    24  // For variables interplation, it has Terraform eval context.
    25  // After checking, it accumulates results as issues.
    26  type Runner struct {
    27  	TFConfig  *configs.Config
    28  	Issues    issue.Issues
    29  	AwsClient *client.AwsClient
    30  
    31  	ctx    terraform.BuiltinEvalContext
    32  	config *Config
    33  }
    34  
    35  // Rule is interface for building the issue
    36  type Rule interface {
    37  	Name() string
    38  	Type() string
    39  	Link() string
    40  }
    41  
    42  // NewRunner returns new TFLint runner
    43  // It prepares built-in context (workpace metadata, variables) from
    44  // received `configs.Config` and `terraform.InputValues`
    45  func NewRunner(c *Config, cfg *configs.Config, variables ...terraform.InputValues) *Runner {
    46  	path := "root"
    47  	if !cfg.Path.IsRoot() {
    48  		path = cfg.Path.String()
    49  	}
    50  	log.Printf("[INFO] Initialize new runner for %s", path)
    51  
    52  	return &Runner{
    53  		TFConfig:  cfg,
    54  		Issues:    []*issue.Issue{},
    55  		AwsClient: client.NewAwsClient(c.AwsCredentials),
    56  
    57  		ctx: terraform.BuiltinEvalContext{
    58  			Evaluator: &terraform.Evaluator{
    59  				Meta: &terraform.ContextMeta{
    60  					Env: getTFWorkspace(),
    61  				},
    62  				Config:             cfg,
    63  				VariableValues:     prepareVariableValues(cfg.Module.Variables, variables...),
    64  				VariableValuesLock: &sync.Mutex{},
    65  			},
    66  		},
    67  		config: c,
    68  	}
    69  }
    70  
    71  // NewModuleRunners returns new TFLint runners for child modules
    72  // Recursively search modules and generate Runners
    73  // In order to propagate attributes of moduleCall as variables to the module,
    74  // evaluate the variables. If it cannot be evaluated, treat it as unknown
    75  func NewModuleRunners(parent *Runner) ([]*Runner, error) {
    76  	runners := []*Runner{}
    77  
    78  	for name, cfg := range parent.TFConfig.Children {
    79  		moduleCall, ok := parent.TFConfig.Module.ModuleCalls[name]
    80  		if !ok {
    81  			panic(fmt.Errorf("Expected module call `%s` is not found in `%s`", name, parent.TFConfig.Path.String()))
    82  		}
    83  		if parent.TFConfig.Path.IsRoot() && parent.config.IgnoreModule[moduleCall.SourceAddr] {
    84  			log.Printf("[INFO] Ignore `%s` module", moduleCall.Name)
    85  			continue
    86  		}
    87  
    88  		attributes, diags := moduleCall.Config.JustAttributes()
    89  		if diags.HasErrors() {
    90  			var causeErr error
    91  			if diags[0].Subject == nil {
    92  				// HACK: When Subject is nil, it outputs unintended message, so it replaces with actual file.
    93  				causeErr = errors.New(strings.Replace(diags.Error(), "<nil>: ", "", 1))
    94  			} else {
    95  				causeErr = diags
    96  			}
    97  			err := &Error{
    98  				Code:  UnexpectedAttributeError,
    99  				Level: ErrorLevel,
   100  				Message: fmt.Sprintf(
   101  					"Attribute of module not allowed was found in %s:%d",
   102  					parent.getFileName(moduleCall.DeclRange.Filename),
   103  					moduleCall.DeclRange.Start.Line,
   104  				),
   105  				Cause: causeErr,
   106  			}
   107  			log.Printf("[ERROR] %s", err)
   108  			return runners, err
   109  		}
   110  
   111  		for varName, rawVar := range cfg.Module.Variables {
   112  			if attribute, exists := attributes[varName]; exists {
   113  				if isEvaluable(attribute.Expr) {
   114  					val, diags := parent.ctx.EvaluateExpr(attribute.Expr, cty.DynamicPseudoType, nil)
   115  					if diags.HasErrors() {
   116  						err := &Error{
   117  							Code:  EvaluationError,
   118  							Level: ErrorLevel,
   119  							Message: fmt.Sprintf(
   120  								"Failed to eval an expression in %s:%d",
   121  								parent.getFileName(attribute.Expr.Range().Filename),
   122  								attribute.Expr.Range().Start.Line,
   123  							),
   124  							Cause: diags.Err(),
   125  						}
   126  						log.Printf("[ERROR] %s", err)
   127  						return runners, err
   128  					}
   129  					rawVar.Default = val
   130  				} else {
   131  					// If module attributes are not evaluable, it marks that value as unknown.
   132  					// Unknown values are ignored when evaluated inside the module.
   133  					log.Printf("[DEBUG] `%s` has been marked as unknown", varName)
   134  					rawVar.Default = cty.UnknownVal(cty.DynamicPseudoType)
   135  				}
   136  			}
   137  		}
   138  
   139  		runner := NewRunner(parent.config, cfg)
   140  		runners = append(runners, runner)
   141  		moudleRunners, err := NewModuleRunners(runner)
   142  		if err != nil {
   143  			return runners, err
   144  		}
   145  		runners = append(runners, moudleRunners...)
   146  	}
   147  
   148  	return runners, nil
   149  }
   150  
   151  // EvaluateExpr is a wrapper of terraform.BultinEvalContext.EvaluateExpr and gocty.FromCtyValue
   152  // When it received slice as `ret`, it converts cty.Value to expected list type
   153  // because raw cty.Value has TupleType.
   154  func (r *Runner) EvaluateExpr(expr hcl.Expression, ret interface{}) error {
   155  	if !isEvaluable(expr) {
   156  		err := &Error{
   157  			Code:  UnevaluableError,
   158  			Level: WarningLevel,
   159  			Message: fmt.Sprintf(
   160  				"Unevaluable expression found in %s:%d",
   161  				r.getFileName(expr.Range().Filename),
   162  				expr.Range().Start.Line,
   163  			),
   164  		}
   165  		log.Printf("[WARN] %s; TFLint ignores an unevaluable expression.", err)
   166  		return err
   167  	}
   168  
   169  	val, diags := r.ctx.EvaluateExpr(expr, cty.DynamicPseudoType, nil)
   170  	if diags.HasErrors() {
   171  		err := &Error{
   172  			Code:  EvaluationError,
   173  			Level: ErrorLevel,
   174  			Message: fmt.Sprintf(
   175  				"Failed to eval an expression in %s:%d",
   176  				r.getFileName(expr.Range().Filename),
   177  				expr.Range().Start.Line,
   178  			),
   179  			Cause: diags.Err(),
   180  		}
   181  		log.Printf("[ERROR] %s", err)
   182  		return err
   183  	}
   184  
   185  	err := cty.Walk(val, func(path cty.Path, v cty.Value) (bool, error) {
   186  		if !v.IsKnown() {
   187  			err := &Error{
   188  				Code:  UnknownValueError,
   189  				Level: WarningLevel,
   190  				Message: fmt.Sprintf(
   191  					"Unknown value found in %s:%d; Please use environment variables or tfvars to set the value",
   192  					r.getFileName(expr.Range().Filename),
   193  					expr.Range().Start.Line,
   194  				),
   195  			}
   196  			log.Printf("[WARN] %s; TFLint ignores an expression includes an unknown value.", err)
   197  			return false, err
   198  		}
   199  
   200  		if v.IsNull() {
   201  			err := &Error{
   202  				Code:  NullValueError,
   203  				Level: WarningLevel,
   204  				Message: fmt.Sprintf(
   205  					"Null value found in %s:%d",
   206  					r.getFileName(expr.Range().Filename),
   207  					expr.Range().Start.Line,
   208  				),
   209  			}
   210  			log.Printf("[WARN] %s; TFLint ignores an expression includes an null value.", err)
   211  			return false, err
   212  		}
   213  
   214  		return true, nil
   215  	})
   216  
   217  	if err != nil {
   218  		return err
   219  	}
   220  
   221  	switch ret.(type) {
   222  	case *string:
   223  		val, err = convert.Convert(val, cty.String)
   224  	case *int:
   225  		val, err = convert.Convert(val, cty.Number)
   226  	case *[]string:
   227  		val, err = convert.Convert(val, cty.List(cty.String))
   228  	case *[]int:
   229  		val, err = convert.Convert(val, cty.List(cty.Number))
   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 expression in %s:%d",
   242  				r.getFileName(expr.Range().Filename),
   243  				expr.Range().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 expression in %s:%d",
   258  				r.getFileName(expr.Range().Filename),
   259  				expr.Range().Start.Line,
   260  			),
   261  			Cause: err,
   262  		}
   263  		log.Printf("[ERROR] %s", err)
   264  		return err
   265  	}
   266  	return nil
   267  }
   268  
   269  // TFConfigPath is a wrapper of addrs.Module
   270  func (r *Runner) TFConfigPath() string {
   271  	if r.TFConfig.Path.IsRoot() {
   272  		return "root"
   273  	}
   274  	return r.TFConfig.Path.String()
   275  }
   276  
   277  // LookupIssues returns issues according to the received files
   278  func (r *Runner) LookupIssues(files ...string) issue.Issues {
   279  	if len(files) == 0 {
   280  		return r.Issues
   281  	}
   282  
   283  	issues := []*issue.Issue{}
   284  	for _, issue := range r.Issues {
   285  		for _, file := range files {
   286  			if file == issue.File {
   287  				issues = append(issues, issue)
   288  			}
   289  		}
   290  	}
   291  	return issues
   292  }
   293  
   294  // WalkResourceAttributes searches for resources and passes the appropriate attributes to the walker function
   295  func (r *Runner) WalkResourceAttributes(resource, attributeName string, walker func(*hcl.Attribute) error) error {
   296  	for _, resource := range r.LookupResourcesByType(resource) {
   297  		body, _, diags := resource.Config.PartialContent(&hcl.BodySchema{
   298  			Attributes: []hcl.AttributeSchema{
   299  				{
   300  					Name: attributeName,
   301  				},
   302  			},
   303  		})
   304  		if diags.HasErrors() {
   305  			return diags
   306  		}
   307  
   308  		if attribute, ok := body.Attributes[attributeName]; ok {
   309  			log.Printf("[DEBUG] Walk `%s` attribute", resource.Type+"."+resource.Name+"."+attributeName)
   310  			err := walker(attribute)
   311  			if err != nil {
   312  				return err
   313  			}
   314  		}
   315  	}
   316  
   317  	return nil
   318  }
   319  
   320  // WalkResourceBlocks walks all blocks of the passed resource and invokes the passed function
   321  func (r *Runner) WalkResourceBlocks(resource, blockType string, walker func(*hcl.Block) error) error {
   322  	for _, resource := range r.LookupResourcesByType(resource) {
   323  		body, _, diags := resource.Config.PartialContent(&hcl.BodySchema{
   324  			Blocks: []hcl.BlockHeaderSchema{
   325  				{
   326  					Type: blockType,
   327  				},
   328  			},
   329  		})
   330  		if diags.HasErrors() {
   331  			return diags
   332  		}
   333  
   334  		for _, block := range body.Blocks {
   335  			log.Printf("[DEBUG] Walk `%s` block", resource.Type+"."+resource.Name+"."+blockType)
   336  			err := walker(block)
   337  			if err != nil {
   338  				return err
   339  			}
   340  		}
   341  	}
   342  
   343  	return nil
   344  }
   345  
   346  // EnsureNoError is a helper for processing when no error occurs
   347  // This function skips processing without returning an error to the caller when the error is warning
   348  func (r *Runner) EnsureNoError(err error, proc func() error) error {
   349  	if err == nil {
   350  		return proc()
   351  	}
   352  
   353  	if appErr, ok := err.(*Error); ok {
   354  		switch appErr.Level {
   355  		case WarningLevel:
   356  			return nil
   357  		case ErrorLevel:
   358  			return appErr
   359  		default:
   360  			panic(appErr)
   361  		}
   362  	} else {
   363  		return err
   364  	}
   365  }
   366  
   367  // IsNullExpr check the passed expression is null
   368  func (r *Runner) IsNullExpr(expr hcl.Expression) bool {
   369  	val, _ := r.ctx.EvaluateExpr(expr, cty.DynamicPseudoType, nil)
   370  	return val.IsNull()
   371  }
   372  
   373  // LookupResourcesByType returns `configs.Resource` list according to the resource type
   374  func (r *Runner) LookupResourcesByType(resourceType string) []*configs.Resource {
   375  	ret := []*configs.Resource{}
   376  
   377  	for _, resource := range r.TFConfig.Module.ManagedResources {
   378  		if resource.Type == resourceType {
   379  			ret = append(ret, resource)
   380  		}
   381  	}
   382  
   383  	return ret
   384  }
   385  
   386  // EachStringSliceExprs iterates an evaluated value and the corresponding expression
   387  // If the given expression is a static list, get an expression for each value
   388  // If not, the given expression is used as it is
   389  func (r *Runner) EachStringSliceExprs(expr hcl.Expression, proc func(val string, expr hcl.Expression)) error {
   390  	var vals []string
   391  	err := r.EvaluateExpr(expr, &vals)
   392  
   393  	exprs, diags := hcl.ExprList(expr)
   394  	if diags.HasErrors() {
   395  		log.Printf("[DEBUG] Expr is not static list: %s", diags)
   396  		for range vals {
   397  			exprs = append(exprs, expr)
   398  		}
   399  	}
   400  
   401  	return r.EnsureNoError(err, func() error {
   402  		for idx, val := range vals {
   403  			proc(val, exprs[idx])
   404  		}
   405  		return nil
   406  	})
   407  }
   408  
   409  // EmitIssue builds an issue and accumulates it
   410  func (r *Runner) EmitIssue(rule Rule, message string, location hcl.Range) {
   411  	r.Issues = append(r.Issues, &issue.Issue{
   412  		Detector: rule.Name(),
   413  		Type:     rule.Type(),
   414  		Message:  message,
   415  		Line:     location.Start.Line,
   416  		File:     r.getFileName(location.Filename),
   417  		Link:     rule.Link(),
   418  	})
   419  }
   420  
   421  // getFileName returns user-friendly file name.
   422  // It returns base file name when processing root module.
   423  // Otherwise, it add the module name as prefix to base file name.
   424  func (r *Runner) getFileName(raw string) string {
   425  	if r.TFConfig.Path.IsRoot() {
   426  		return filepath.Base(raw)
   427  	}
   428  	return filepath.Join(r.TFConfig.Path.String(), filepath.Base(raw))
   429  }
   430  
   431  // prepareVariableValues prepares Terraform variables from configs, input variables and environment variables.
   432  // Variables in the configuration are overwritten by environment variables.
   433  // Finally, they are overwritten by received input variable on the received order.
   434  // Therefore, CLI flag input variables must be passed at the end of arguments.
   435  // This is the responsibility of the caller.
   436  // See https://www.terraform.io/intro/getting-started/variables.html#assigning-variables
   437  func prepareVariableValues(configVars map[string]*configs.Variable, variables ...terraform.InputValues) map[string]map[string]cty.Value {
   438  	overrideVariables := terraform.DefaultVariableValues(configVars).Override(getTFEnvVariables()).Override(variables...)
   439  
   440  	variableValues := make(map[string]map[string]cty.Value)
   441  	variableValues[""] = make(map[string]cty.Value)
   442  	for k, iv := range overrideVariables {
   443  		variableValues[""][k] = iv.Value
   444  	}
   445  	return variableValues
   446  }
   447  
   448  func isEvaluable(expr hcl.Expression) bool {
   449  	refs, diags := lang.ReferencesInExpr(expr)
   450  	if diags.HasErrors() {
   451  		// Maybe this is bug
   452  		panic(diags.Err())
   453  	}
   454  	for _, ref := range refs {
   455  		switch ref.Subject.(type) {
   456  		case addrs.InputVariable:
   457  			// noop
   458  		case addrs.TerraformAttr:
   459  			// noop
   460  		default:
   461  			return false
   462  		}
   463  	}
   464  	return true
   465  }