github.com/jpreese/tflint@v0.19.2-0.20200908152133-b01686250fb6/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  	hcl "github.com/hashicorp/hcl/v2"
    12  	"github.com/hashicorp/hcl/v2/gohcl"
    13  	"github.com/hashicorp/terraform/addrs"
    14  	"github.com/hashicorp/terraform/configs"
    15  	"github.com/hashicorp/terraform/lang"
    16  	"github.com/hashicorp/terraform/terraform"
    17  	"github.com/terraform-linters/tflint/client"
    18  	"github.com/zclconf/go-cty/cty"
    19  )
    20  
    21  // Runner checks templates according rules.
    22  // For variables interplation, it has Terraform eval context.
    23  // After checking, it accumulates results as issues.
    24  type Runner struct {
    25  	TFConfig  *configs.Config
    26  	Issues    Issues
    27  	AwsClient *client.AwsClient
    28  
    29  	ctx         terraform.EvalContext
    30  	files       map[string]*hcl.File
    31  	annotations map[string]Annotations
    32  	config      *Config
    33  	currentExpr hcl.Expression
    34  	modVars     map[string]*moduleVariable
    35  }
    36  
    37  // Rule is interface for building the issue
    38  type Rule interface {
    39  	Name() string
    40  	Severity() string
    41  	Link() string
    42  }
    43  
    44  // NewRunner returns new TFLint runner
    45  // It prepares built-in context (workpace metadata, variables) from
    46  // received `configs.Config` and `terraform.InputValues`
    47  func NewRunner(c *Config, files map[string]*hcl.File, ants map[string]Annotations, cfg *configs.Config, variables ...terraform.InputValues) (*Runner, error) {
    48  	path := "root"
    49  	if !cfg.Path.IsRoot() {
    50  		path = cfg.Path.String()
    51  	}
    52  	log.Printf("[INFO] Initialize new runner for %s", path)
    53  
    54  	ctx := terraform.BuiltinEvalContext{
    55  		Evaluator: &terraform.Evaluator{
    56  			Meta: &terraform.ContextMeta{
    57  				Env: getTFWorkspace(),
    58  			},
    59  			Config:             cfg.Root,
    60  			VariableValues:     prepareVariableValues(cfg, variables...),
    61  			VariableValuesLock: &sync.Mutex{},
    62  		},
    63  	}
    64  
    65  	runner := &Runner{
    66  		TFConfig:  cfg,
    67  		Issues:    Issues{},
    68  		AwsClient: &client.AwsClient{},
    69  
    70  		// TODO: As described in the godoc for UnkeyedInstanceShim,
    71  		// it will need to be replaced now that module.for_each is supported
    72  		ctx:         ctx.WithPath(cfg.Path.UnkeyedInstanceShim()),
    73  		files:       files,
    74  		annotations: ants,
    75  		config:      c,
    76  	}
    77  
    78  	// Initialize client for the root runner
    79  	if c.DeepCheck && cfg.Path.IsRoot() {
    80  		// FIXME: Alias providers are not considered
    81  		providerConfig, err := NewProviderConfig(
    82  			cfg.Module.ProviderConfigs["aws"],
    83  			runner,
    84  			client.AwsProviderBlockSchema,
    85  		)
    86  		if err != nil {
    87  			return nil, err
    88  		}
    89  		creds, err := client.ConvertToCredentials(providerConfig)
    90  		if err != nil {
    91  			return nil, err
    92  		}
    93  
    94  		runner.AwsClient, err = client.NewAwsClient(c.AwsCredentials.Merge(creds))
    95  		if err != nil {
    96  			return nil, err
    97  		}
    98  	}
    99  
   100  	return runner, nil
   101  }
   102  
   103  // NewModuleRunners returns new TFLint runners for child modules
   104  // Recursively search modules and generate Runners
   105  // In order to propagate attributes of moduleCall as variables to the module,
   106  // evaluate the variables. If it cannot be evaluated, treat it as unknown
   107  func NewModuleRunners(parent *Runner) ([]*Runner, error) {
   108  	runners := []*Runner{}
   109  
   110  	for name, cfg := range parent.TFConfig.Children {
   111  		moduleCall, ok := parent.TFConfig.Module.ModuleCalls[name]
   112  		if !ok {
   113  			panic(fmt.Errorf("Expected module call `%s` is not found in `%s`", name, parent.TFConfig.Path.String()))
   114  		}
   115  		if parent.TFConfig.Path.IsRoot() && parent.config.IgnoreModules[moduleCall.SourceAddr] {
   116  			log.Printf("[INFO] Ignore `%s` module", moduleCall.Name)
   117  			continue
   118  		}
   119  
   120  		attributes, diags := moduleCall.Config.JustAttributes()
   121  		if diags.HasErrors() {
   122  			var causeErr error
   123  			if diags[0].Subject == nil {
   124  				// HACK: When Subject is nil, it outputs unintended message, so it replaces with actual file.
   125  				causeErr = errors.New(strings.Replace(diags.Error(), "<nil>: ", "", 1))
   126  			} else {
   127  				causeErr = diags
   128  			}
   129  			err := &Error{
   130  				Code:  UnexpectedAttributeError,
   131  				Level: ErrorLevel,
   132  				Message: fmt.Sprintf(
   133  					"Attribute of module not allowed was found in %s:%d",
   134  					moduleCall.DeclRange.Filename,
   135  					moduleCall.DeclRange.Start.Line,
   136  				),
   137  				Cause: causeErr,
   138  			}
   139  			log.Printf("[ERROR] %s", err)
   140  			return runners, err
   141  		}
   142  
   143  		modVars := map[string]*moduleVariable{}
   144  		for varName, rawVar := range cfg.Module.Variables {
   145  			if attribute, exists := attributes[varName]; exists {
   146  				evalauble, err := isEvaluableExpr(attribute.Expr)
   147  				if err != nil {
   148  					return runners, err
   149  				}
   150  
   151  				if evalauble {
   152  					val, diags := parent.ctx.EvaluateExpr(attribute.Expr, cty.DynamicPseudoType, nil)
   153  					if diags.HasErrors() {
   154  						err := &Error{
   155  							Code:  EvaluationError,
   156  							Level: ErrorLevel,
   157  							Message: fmt.Sprintf(
   158  								"Failed to eval an expression in %s:%d",
   159  								attribute.Expr.Range().Filename,
   160  								attribute.Expr.Range().Start.Line,
   161  							),
   162  							Cause: diags.Err(),
   163  						}
   164  						log.Printf("[ERROR] %s", err)
   165  						return runners, err
   166  					}
   167  					rawVar.Default = val
   168  				} else {
   169  					// If module attributes are not evaluable, it marks that value as unknown.
   170  					// Unknown values are ignored when evaluated inside the module.
   171  					log.Printf("[DEBUG] `%s` has been marked as unknown", varName)
   172  					rawVar.Default = cty.UnknownVal(cty.DynamicPseudoType)
   173  				}
   174  
   175  				if parent.TFConfig.Path.IsRoot() {
   176  					modVars[varName] = &moduleVariable{
   177  						Root:      true,
   178  						DeclRange: attribute.Expr.Range(),
   179  					}
   180  				} else {
   181  					parentVars := []*moduleVariable{}
   182  					for _, ref := range listVarRefs(attribute.Expr) {
   183  						if parentVar, exists := parent.modVars[ref.Name]; exists {
   184  							parentVars = append(parentVars, parentVar)
   185  						}
   186  					}
   187  					modVars[varName] = &moduleVariable{
   188  						Parents:   parentVars,
   189  						DeclRange: attribute.Expr.Range(),
   190  					}
   191  				}
   192  			}
   193  		}
   194  
   195  		runner, err := NewRunner(parent.config, parent.files, parent.annotations, cfg)
   196  		if err != nil {
   197  			return runners, err
   198  		}
   199  		runner.modVars = modVars
   200  		// Inherit parent's AwsClient
   201  		runner.AwsClient = parent.AwsClient
   202  		runners = append(runners, runner)
   203  		moudleRunners, err := NewModuleRunners(runner)
   204  		if err != nil {
   205  			return runners, err
   206  		}
   207  		runners = append(runners, moudleRunners...)
   208  	}
   209  
   210  	return runners, nil
   211  }
   212  
   213  // TFConfigPath is a wrapper of addrs.Module
   214  func (r *Runner) TFConfigPath() string {
   215  	if r.TFConfig.Path.IsRoot() {
   216  		return "root"
   217  	}
   218  	return r.TFConfig.Path.String()
   219  }
   220  
   221  // LookupIssues returns issues according to the received files
   222  func (r *Runner) LookupIssues(files ...string) Issues {
   223  	if len(files) == 0 {
   224  		return r.Issues
   225  	}
   226  
   227  	issues := Issues{}
   228  	for _, issue := range r.Issues {
   229  		for _, file := range files {
   230  			if file == issue.Range.Filename {
   231  				issues = append(issues, issue)
   232  			}
   233  		}
   234  	}
   235  	return issues
   236  }
   237  
   238  // File returns the raw *hcl.File representation of a Terraform configuration at the specified path,
   239  // or nil if there path does not match any configuration.
   240  func (r *Runner) File(path string) *hcl.File {
   241  	return r.files[path]
   242  }
   243  
   244  // Files returns the raw *hcl.File representation of all Terraform configuration in the module directory.
   245  func (r *Runner) Files() map[string]*hcl.File {
   246  	result := make(map[string]*hcl.File)
   247  	for name, file := range r.files {
   248  		if filepath.Dir(name) == r.TFConfig.Module.SourceDir {
   249  			result[name] = file
   250  		}
   251  	}
   252  	return result
   253  }
   254  
   255  // Backend returns the backend configuration.
   256  func (r *Runner) Backend() *configs.Backend {
   257  	return r.TFConfig.Module.Backend
   258  }
   259  
   260  // EnsureNoError is a helper for processing when no error occurs
   261  // This function skips processing without returning an error to the caller when the error is warning
   262  func (r *Runner) EnsureNoError(err error, proc func() error) error {
   263  	if err == nil {
   264  		return proc()
   265  	}
   266  
   267  	if appErr, ok := err.(*Error); ok {
   268  		switch appErr.Level {
   269  		case WarningLevel:
   270  			return nil
   271  		case ErrorLevel:
   272  			return appErr
   273  		default:
   274  			panic(appErr)
   275  		}
   276  	} else {
   277  		return err
   278  	}
   279  }
   280  
   281  // IsNullExpr check the passed expression is null
   282  func (r *Runner) IsNullExpr(expr hcl.Expression) (bool, error) {
   283  	evaluable, err := isEvaluableExpr(expr)
   284  	if err != nil {
   285  		return false, err
   286  	}
   287  
   288  	if !evaluable {
   289  		return false, nil
   290  	}
   291  	val, diags := r.ctx.EvaluateExpr(expr, cty.DynamicPseudoType, nil)
   292  	if diags.HasErrors() {
   293  		return false, diags.Err()
   294  	}
   295  	return val.IsNull(), nil
   296  }
   297  
   298  // LookupResourcesByType returns `configs.Resource` list according to the resource type
   299  func (r *Runner) LookupResourcesByType(resourceType string) []*configs.Resource {
   300  	ret := []*configs.Resource{}
   301  
   302  	for _, resource := range r.TFConfig.Module.ManagedResources {
   303  		if resource.Type == resourceType {
   304  			ret = append(ret, resource)
   305  		}
   306  	}
   307  
   308  	return ret
   309  }
   310  
   311  // EachStringSliceExprs iterates an evaluated value and the corresponding expression
   312  // If the given expression is a static list, get an expression for each value
   313  // If not, the given expression is used as it is
   314  func (r *Runner) EachStringSliceExprs(expr hcl.Expression, proc func(val string, expr hcl.Expression)) error {
   315  	var vals []string
   316  	err := r.EvaluateExpr(expr, &vals)
   317  
   318  	exprs, diags := hcl.ExprList(expr)
   319  	if diags.HasErrors() {
   320  		log.Printf("[DEBUG] Expr is not static list: %s", diags)
   321  		for range vals {
   322  			exprs = append(exprs, expr)
   323  		}
   324  	}
   325  
   326  	return r.EnsureNoError(err, func() error {
   327  		for idx, val := range vals {
   328  			proc(val, exprs[idx])
   329  		}
   330  		return nil
   331  	})
   332  }
   333  
   334  // EmitIssue builds an issue and accumulates it
   335  func (r *Runner) EmitIssue(rule Rule, message string, location hcl.Range) {
   336  	if r.TFConfig.Path.IsRoot() {
   337  		r.emitIssue(&Issue{
   338  			Rule:    rule,
   339  			Message: message,
   340  			Range:   location,
   341  		})
   342  	} else {
   343  		for _, modVar := range r.listModuleVars(r.currentExpr) {
   344  			r.emitIssue(&Issue{
   345  				Rule:    rule,
   346  				Message: message,
   347  				Range:   modVar.DeclRange,
   348  				Callers: append(modVar.callers(), location),
   349  			})
   350  		}
   351  	}
   352  }
   353  
   354  // WithExpressionContext sets the context of the passed expression currently being processed.
   355  func (r *Runner) WithExpressionContext(expr hcl.Expression, proc func() error) error {
   356  	r.currentExpr = expr
   357  	err := proc()
   358  	r.currentExpr = nil
   359  	return err
   360  }
   361  
   362  // DecodeRuleConfig extracts the rule's configuration into the given value
   363  func (r *Runner) DecodeRuleConfig(ruleName string, val interface{}) error {
   364  	if rule, exists := r.config.Rules[ruleName]; exists {
   365  		diags := gohcl.DecodeBody(rule.Body, nil, val)
   366  		if diags.HasErrors() {
   367  			// HACK: If you enable the rule through the CLI instead of the file, its hcl.Body will not contain valid range.
   368  			// @see https://github.com/hashicorp/hcl/blob/v2.5.0/merged.go#L132-L135
   369  			if rule.Body.MissingItemRange().Filename == "<empty>" {
   370  				return errors.New("This rule cannot be enabled with the `--enable-rule` option because it lacks the required configuration")
   371  			}
   372  			return diags
   373  		}
   374  	}
   375  	return nil
   376  }
   377  
   378  func (r *Runner) emitIssue(issue *Issue) {
   379  	if annotations, ok := r.annotations[issue.Range.Filename]; ok {
   380  		for _, annotation := range annotations {
   381  			if annotation.IsAffected(issue) {
   382  				log.Printf("[INFO] %s (%s) is ignored by %s", issue.Range.String(), issue.Rule.Name(), annotation.String())
   383  				return
   384  			}
   385  		}
   386  	}
   387  	r.Issues = append(r.Issues, issue)
   388  }
   389  
   390  func (r *Runner) listModuleVars(expr hcl.Expression) []*moduleVariable {
   391  	ret := []*moduleVariable{}
   392  	for _, ref := range listVarRefs(expr) {
   393  		if modVar, exists := r.modVars[ref.Name]; exists {
   394  			ret = append(ret, modVar.roots()...)
   395  		}
   396  	}
   397  	return ret
   398  }
   399  
   400  // prepareVariableValues builds variableValues from configs, input variables and environment variables.
   401  // Variables which declared in the configuration are overwritten by environment variables.
   402  // Finally, they are overwritten by input variables in the order passed.
   403  // Therefore, CLI flag input variables must be passed at the end of arguments.
   404  // This is the responsibility of the caller.
   405  // See https://learn.hashicorp.com/terraform/getting-started/variables.html#assigning-variables
   406  func prepareVariableValues(config *configs.Config, variables ...terraform.InputValues) map[string]map[string]cty.Value {
   407  	moduleKey := config.Path.UnkeyedInstanceShim().String()
   408  	overrideVariables := terraform.DefaultVariableValues(config.Module.Variables).Override(getTFEnvVariables()).Override(variables...)
   409  
   410  	variableValues := make(map[string]map[string]cty.Value)
   411  	variableValues[moduleKey] = make(map[string]cty.Value)
   412  	for k, iv := range overrideVariables {
   413  		variableValues[moduleKey][k] = iv.Value
   414  	}
   415  	return variableValues
   416  }
   417  
   418  func listVarRefs(expr hcl.Expression) []addrs.InputVariable {
   419  	refs, diags := lang.ReferencesInExpr(expr)
   420  	if diags.HasErrors() {
   421  		// Maybe this is bug
   422  		panic(diags.Err())
   423  	}
   424  
   425  	ret := []addrs.InputVariable{}
   426  	for _, ref := range refs {
   427  		if varRef, ok := ref.Subject.(addrs.InputVariable); ok {
   428  			ret = append(ret, varRef)
   429  		}
   430  	}
   431  
   432  	return ret
   433  }