github.com/terraform-linters/tflint-plugin-sdk@v0.22.0/helper/runner.go (about)

     1  package helper
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"os"
     7  	"reflect"
     8  
     9  	"github.com/hashicorp/hcl/v2"
    10  	"github.com/hashicorp/hcl/v2/gohcl"
    11  	"github.com/hashicorp/hcl/v2/hclsyntax"
    12  	"github.com/terraform-linters/tflint-plugin-sdk/hclext"
    13  	"github.com/terraform-linters/tflint-plugin-sdk/internal"
    14  	"github.com/terraform-linters/tflint-plugin-sdk/terraform/addrs"
    15  	"github.com/terraform-linters/tflint-plugin-sdk/terraform/lang/marks"
    16  	"github.com/terraform-linters/tflint-plugin-sdk/tflint"
    17  	"github.com/zclconf/go-cty/cty"
    18  	"github.com/zclconf/go-cty/cty/convert"
    19  	"github.com/zclconf/go-cty/cty/gocty"
    20  )
    21  
    22  // Runner is a mock that satisfies the Runner interface for plugin testing.
    23  type Runner struct {
    24  	Issues Issues
    25  
    26  	files     map[string]*hcl.File
    27  	sources   map[string][]byte
    28  	config    Config
    29  	variables map[string]*Variable
    30  	fixer     *internal.Fixer
    31  }
    32  
    33  // Variable is an implementation of variables in Terraform language
    34  type Variable struct {
    35  	Name      string
    36  	Default   cty.Value
    37  	DeclRange hcl.Range
    38  }
    39  
    40  // Config is a pseudo TFLint config file object for testing from plugins.
    41  type Config struct {
    42  	Rules []RuleConfig `hcl:"rule,block"`
    43  }
    44  
    45  // RuleConfig is a pseudo TFLint config file object for testing from plugins.
    46  type RuleConfig struct {
    47  	Name    string   `hcl:"name,label"`
    48  	Enabled bool     `hcl:"enabled"`
    49  	Body    hcl.Body `hcl:",remain"`
    50  }
    51  
    52  var _ tflint.Runner = &Runner{}
    53  
    54  // GetOriginalwd always returns the current directory
    55  func (r *Runner) GetOriginalwd() (string, error) {
    56  	return os.Getwd()
    57  }
    58  
    59  // GetModulePath always returns the root module path address
    60  func (r *Runner) GetModulePath() (addrs.Module, error) {
    61  	return []string{}, nil
    62  }
    63  
    64  // GetModuleContent gets a content of the current module
    65  func (r *Runner) GetModuleContent(schema *hclext.BodySchema, opts *tflint.GetModuleContentOption) (*hclext.BodyContent, error) {
    66  	content := &hclext.BodyContent{}
    67  	diags := hcl.Diagnostics{}
    68  
    69  	for _, f := range r.files {
    70  		c, d := hclext.PartialContent(f.Body, schema)
    71  		diags = diags.Extend(d)
    72  		for name, attr := range c.Attributes {
    73  			content.Attributes[name] = attr
    74  		}
    75  		content.Blocks = append(content.Blocks, c.Blocks...)
    76  	}
    77  
    78  	if diags.HasErrors() {
    79  		return nil, diags
    80  	}
    81  	return content, nil
    82  }
    83  
    84  // GetResourceContent gets a resource content of the current module
    85  func (r *Runner) GetResourceContent(name string, schema *hclext.BodySchema, opts *tflint.GetModuleContentOption) (*hclext.BodyContent, error) {
    86  	body, err := r.GetModuleContent(&hclext.BodySchema{
    87  		Blocks: []hclext.BlockSchema{
    88  			{Type: "resource", LabelNames: []string{"type", "name"}, Body: schema},
    89  		},
    90  	}, opts)
    91  	if err != nil {
    92  		return nil, err
    93  	}
    94  
    95  	content := &hclext.BodyContent{Blocks: []*hclext.Block{}}
    96  	for _, resource := range body.Blocks {
    97  		if resource.Labels[0] != name {
    98  			continue
    99  		}
   100  
   101  		content.Blocks = append(content.Blocks, resource)
   102  	}
   103  
   104  	return content, nil
   105  }
   106  
   107  // GetProviderContent gets a provider content of the current module
   108  func (r *Runner) GetProviderContent(name string, schema *hclext.BodySchema, opts *tflint.GetModuleContentOption) (*hclext.BodyContent, error) {
   109  	body, err := r.GetModuleContent(&hclext.BodySchema{
   110  		Blocks: []hclext.BlockSchema{
   111  			{Type: "provider", LabelNames: []string{"name"}, Body: schema},
   112  		},
   113  	}, opts)
   114  	if err != nil {
   115  		return nil, err
   116  	}
   117  
   118  	content := &hclext.BodyContent{Blocks: []*hclext.Block{}}
   119  	for _, provider := range body.Blocks {
   120  		if provider.Labels[0] != name {
   121  			continue
   122  		}
   123  
   124  		content.Blocks = append(content.Blocks, provider)
   125  	}
   126  
   127  	return content, nil
   128  }
   129  
   130  // GetFile returns the hcl.File object
   131  func (r *Runner) GetFile(filename string) (*hcl.File, error) {
   132  	return r.files[filename], nil
   133  }
   134  
   135  // GetFiles returns all hcl.File
   136  func (r *Runner) GetFiles() (map[string]*hcl.File, error) {
   137  	return r.files, nil
   138  }
   139  
   140  type nativeWalker struct {
   141  	walker tflint.ExprWalker
   142  }
   143  
   144  func (w *nativeWalker) Enter(node hclsyntax.Node) hcl.Diagnostics {
   145  	if expr, ok := node.(hcl.Expression); ok {
   146  		return w.walker.Enter(expr)
   147  	}
   148  	return nil
   149  }
   150  
   151  func (w *nativeWalker) Exit(node hclsyntax.Node) hcl.Diagnostics {
   152  	if expr, ok := node.(hcl.Expression); ok {
   153  		return w.walker.Exit(expr)
   154  	}
   155  	return nil
   156  }
   157  
   158  // WalkExpressions traverses expressions in all files by the passed walker.
   159  func (r *Runner) WalkExpressions(walker tflint.ExprWalker) hcl.Diagnostics {
   160  	diags := hcl.Diagnostics{}
   161  	for _, file := range r.files {
   162  		if body, ok := file.Body.(*hclsyntax.Body); ok {
   163  			walkDiags := hclsyntax.Walk(body, &nativeWalker{walker: walker})
   164  			diags = diags.Extend(walkDiags)
   165  			continue
   166  		}
   167  
   168  		// In JSON syntax, everything can be walked as an attribute.
   169  		attrs, jsonDiags := file.Body.JustAttributes()
   170  		if jsonDiags.HasErrors() {
   171  			diags = diags.Extend(jsonDiags)
   172  			continue
   173  		}
   174  
   175  		for _, attr := range attrs {
   176  			enterDiags := walker.Enter(attr.Expr)
   177  			diags = diags.Extend(enterDiags)
   178  			exitDiags := walker.Exit(attr.Expr)
   179  			diags = diags.Extend(exitDiags)
   180  		}
   181  	}
   182  
   183  	return diags
   184  }
   185  
   186  // DecodeRuleConfig extracts the rule's configuration into the given value
   187  func (r *Runner) DecodeRuleConfig(name string, ret interface{}) error {
   188  	schema := hclext.ImpliedBodySchema(ret)
   189  
   190  	for _, rule := range r.config.Rules {
   191  		if rule.Name == name {
   192  			body, diags := hclext.Content(rule.Body, schema)
   193  			if diags.HasErrors() {
   194  				return diags
   195  			}
   196  			if diags := hclext.DecodeBody(body, nil, ret); diags.HasErrors() {
   197  				return diags
   198  			}
   199  			return nil
   200  		}
   201  	}
   202  
   203  	return nil
   204  }
   205  
   206  var errRefTy = reflect.TypeOf((*error)(nil)).Elem()
   207  
   208  // EvaluateExpr returns a value of the passed expression.
   209  // Note that some features are limited
   210  func (r *Runner) EvaluateExpr(expr hcl.Expression, target interface{}, opts *tflint.EvaluateExprOption) error {
   211  	rval := reflect.ValueOf(target)
   212  	rty := rval.Type()
   213  
   214  	var callback bool
   215  	switch rty.Kind() {
   216  	case reflect.Func:
   217  		// Callback must meet the following requirements:
   218  		//   - It must be a function
   219  		//   - It must take an argument
   220  		//   - It must return an error
   221  		if !(rty.NumIn() == 1 && rty.NumOut() == 1 && rty.Out(0).Implements(errRefTy)) {
   222  			panic(`callback must be of type "func (v T) error"`)
   223  		}
   224  		callback = true
   225  		target = reflect.New(rty.In(0)).Interface()
   226  
   227  	case reflect.Pointer:
   228  		// ok
   229  	default:
   230  		panic("target value is not a pointer or function")
   231  	}
   232  
   233  	err := r.evaluateExpr(expr, target, opts)
   234  	if !callback {
   235  		// error should be handled in the caller
   236  		return err
   237  	}
   238  
   239  	if err != nil {
   240  		// If it cannot be represented as a Go value, exit without invoking the callback rather than returning an error.
   241  		if errors.Is(err, tflint.ErrUnknownValue) || errors.Is(err, tflint.ErrNullValue) || errors.Is(err, tflint.ErrSensitive) || errors.Is(err, tflint.ErrUnevaluable) {
   242  			return nil
   243  		}
   244  		return err
   245  	}
   246  
   247  	rerr := rval.Call([]reflect.Value{reflect.ValueOf(target).Elem()})
   248  	if rerr[0].IsNil() {
   249  		return nil
   250  	}
   251  	return rerr[0].Interface().(error)
   252  }
   253  
   254  func (r *Runner) evaluateExpr(expr hcl.Expression, target interface{}, opts *tflint.EvaluateExprOption) error {
   255  	if opts == nil {
   256  		opts = &tflint.EvaluateExprOption{}
   257  	}
   258  
   259  	var ty cty.Type
   260  	if opts.WantType != nil {
   261  		ty = *opts.WantType
   262  	} else {
   263  		switch target.(type) {
   264  		case *string:
   265  			ty = cty.String
   266  		case *int:
   267  			ty = cty.Number
   268  		case *bool:
   269  			ty = cty.Bool
   270  		case *[]string:
   271  			ty = cty.List(cty.String)
   272  		case *[]int:
   273  			ty = cty.List(cty.Number)
   274  		case *[]bool:
   275  			ty = cty.List(cty.Bool)
   276  		case *map[string]string:
   277  			ty = cty.Map(cty.String)
   278  		case *map[string]int:
   279  			ty = cty.Map(cty.Number)
   280  		case *map[string]bool:
   281  			ty = cty.Map(cty.Bool)
   282  		case *cty.Value:
   283  			ty = cty.DynamicPseudoType
   284  		default:
   285  			return fmt.Errorf("unsupported target type: %T", target)
   286  		}
   287  	}
   288  
   289  	variables := map[string]cty.Value{}
   290  	for _, variable := range r.variables {
   291  		variables[variable.Name] = variable.Default
   292  	}
   293  	workspace, success := os.LookupEnv("TF_WORKSPACE")
   294  	if !success {
   295  		workspace = "default"
   296  	}
   297  	rawVal, diags := expr.Value(&hcl.EvalContext{
   298  		Variables: map[string]cty.Value{
   299  			"var": cty.ObjectVal(variables),
   300  			"terraform": cty.ObjectVal(map[string]cty.Value{
   301  				"workspace": cty.StringVal(workspace),
   302  			}),
   303  		},
   304  	})
   305  	if diags.HasErrors() {
   306  		return diags
   307  	}
   308  	val, err := convert.Convert(rawVal, ty)
   309  	if err != nil {
   310  		return err
   311  	}
   312  
   313  	return gocty.FromCtyValue(val, target)
   314  }
   315  
   316  // EmitIssue adds an issue to the runner itself.
   317  func (r *Runner) EmitIssue(rule tflint.Rule, message string, location hcl.Range) error {
   318  	r.Issues = append(r.Issues, &Issue{
   319  		Rule:    rule,
   320  		Message: message,
   321  		Range:   location,
   322  	})
   323  	return nil
   324  }
   325  
   326  // EmitIssueWithFix adds an issue and invoke fix.
   327  func (r *Runner) EmitIssueWithFix(rule tflint.Rule, message string, location hcl.Range, fixFunc func(f tflint.Fixer) error) error {
   328  	r.fixer.StashChanges()
   329  	if err := fixFunc(r.fixer); err != nil {
   330  		if errors.Is(err, tflint.ErrFixNotSupported) {
   331  			r.fixer.PopChangesFromStash()
   332  			return r.EmitIssue(rule, message, location)
   333  		}
   334  		return err
   335  	}
   336  	return r.EmitIssue(rule, message, location)
   337  }
   338  
   339  // Changes returns formatted changes by the fixer.
   340  func (r *Runner) Changes() map[string][]byte {
   341  	r.fixer.FormatChanges()
   342  	return r.fixer.Changes()
   343  }
   344  
   345  // EnsureNoError is a method that simply runs a function if there is no error.
   346  //
   347  // Deprecated: Use EvaluateExpr with a function callback. e.g. EvaluateExpr(expr, func (val T) error {}, ...)
   348  func (r *Runner) EnsureNoError(err error, proc func() error) error {
   349  	if err == nil {
   350  		return proc()
   351  	}
   352  	return err
   353  }
   354  
   355  // newLocalRunner initialises a new test runner.
   356  func newLocalRunner(files map[string]*hcl.File, issues Issues) *Runner {
   357  	return &Runner{
   358  		files:     map[string]*hcl.File{},
   359  		sources:   map[string][]byte{},
   360  		variables: map[string]*Variable{},
   361  		Issues:    issues,
   362  	}
   363  }
   364  
   365  // addLocalFile adds a new file to the current mapped files.
   366  // For testing only. Normally, the main TFLint process is responsible for loading files.
   367  func (r *Runner) addLocalFile(name string, file *hcl.File) bool {
   368  	if _, exists := r.files[name]; exists {
   369  		return false
   370  	}
   371  
   372  	r.files[name] = file
   373  	r.sources[name] = file.Bytes
   374  	return true
   375  }
   376  
   377  // initFromFiles initializes the runner from locally added files.
   378  // For testing only.
   379  func (r *Runner) initFromFiles() error {
   380  	for _, file := range r.files {
   381  		content, _, diags := file.Body.PartialContent(configFileSchema)
   382  		if diags.HasErrors() {
   383  			return diags
   384  		}
   385  
   386  		for _, block := range content.Blocks {
   387  			switch block.Type {
   388  			case "variable":
   389  				variable, diags := decodeVariableBlock(block)
   390  				if diags.HasErrors() {
   391  					return diags
   392  				}
   393  				r.variables[variable.Name] = variable
   394  			default:
   395  				continue
   396  			}
   397  		}
   398  	}
   399  	r.fixer = internal.NewFixer(r.sources)
   400  
   401  	return nil
   402  }
   403  
   404  func decodeVariableBlock(block *hcl.Block) (*Variable, hcl.Diagnostics) {
   405  	v := &Variable{
   406  		Name:      block.Labels[0],
   407  		DeclRange: block.DefRange,
   408  	}
   409  
   410  	content, _, diags := block.Body.PartialContent(&hcl.BodySchema{
   411  		Attributes: []hcl.AttributeSchema{
   412  			{
   413  				Name: "default",
   414  			},
   415  			{
   416  				Name: "sensitive",
   417  			},
   418  			{
   419  				Name: "ephemeral",
   420  			},
   421  		},
   422  	})
   423  	if diags.HasErrors() {
   424  		return v, diags
   425  	}
   426  
   427  	if attr, exists := content.Attributes["default"]; exists {
   428  		val, diags := attr.Expr.Value(nil)
   429  		if diags.HasErrors() {
   430  			return v, diags
   431  		}
   432  
   433  		v.Default = val
   434  	}
   435  	if attr, exists := content.Attributes["sensitive"]; exists {
   436  		var sensitive bool
   437  		diags := gohcl.DecodeExpression(attr.Expr, nil, &sensitive)
   438  		if diags.HasErrors() {
   439  			return v, diags
   440  		}
   441  
   442  		v.Default = v.Default.Mark(marks.Sensitive)
   443  	}
   444  	if attr, exists := content.Attributes["ephemeral"]; exists {
   445  		var ephemeral bool
   446  		diags := gohcl.DecodeExpression(attr.Expr, nil, &ephemeral)
   447  		if diags.HasErrors() {
   448  			return v, diags
   449  		}
   450  
   451  		v.Default = v.Default.Mark(marks.Ephemeral)
   452  	}
   453  
   454  	return v, nil
   455  }
   456  
   457  var configFileSchema = &hcl.BodySchema{
   458  	Blocks: []hcl.BlockHeaderSchema{
   459  		{
   460  			Type:       "variable",
   461  			LabelNames: []string{"name"},
   462  		},
   463  	},
   464  }