github.com/terraform-linters/tflint-plugin-sdk@v0.22.0/plugin/internal/plugin2host/client.go (about)

     1  package plugin2host
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"os"
     8  	"reflect"
     9  	"strings"
    10  
    11  	"github.com/hashicorp/hcl/v2"
    12  	"github.com/hashicorp/hcl/v2/hclsyntax"
    13  	hcljson "github.com/hashicorp/hcl/v2/json"
    14  	"github.com/terraform-linters/tflint-plugin-sdk/hclext"
    15  	"github.com/terraform-linters/tflint-plugin-sdk/internal"
    16  	"github.com/terraform-linters/tflint-plugin-sdk/logger"
    17  	"github.com/terraform-linters/tflint-plugin-sdk/plugin/internal/fromproto"
    18  	"github.com/terraform-linters/tflint-plugin-sdk/plugin/internal/proto"
    19  	"github.com/terraform-linters/tflint-plugin-sdk/plugin/internal/toproto"
    20  	"github.com/terraform-linters/tflint-plugin-sdk/terraform/addrs"
    21  	"github.com/terraform-linters/tflint-plugin-sdk/terraform/lang/marks"
    22  	"github.com/terraform-linters/tflint-plugin-sdk/tflint"
    23  	"github.com/zclconf/go-cty/cty"
    24  	"github.com/zclconf/go-cty/cty/gocty"
    25  	"github.com/zclconf/go-cty/cty/json"
    26  	"google.golang.org/grpc/codes"
    27  	"google.golang.org/grpc/status"
    28  )
    29  
    30  // GRPCClient is a plugin-side implementation. Plugin can send requests through the client to host's gRPC server.
    31  type GRPCClient struct {
    32  	Client     proto.RunnerClient
    33  	Fixer      *internal.Fixer
    34  	FixEnabled bool
    35  }
    36  
    37  var _ tflint.Runner = &GRPCClient{}
    38  
    39  // GetOriginalwd gets the original working directory.
    40  func (c *GRPCClient) GetOriginalwd() (string, error) {
    41  	resp, err := c.Client.GetOriginalwd(context.Background(), &proto.GetOriginalwd_Request{})
    42  	if err != nil {
    43  		if st, ok := status.FromError(err); ok && st.Code() == codes.Unimplemented {
    44  			// Originalwd is available in TFLint v0.44+
    45  			// Fallback to os.Getwd() because it equals the current directory in earlier versions.
    46  			return os.Getwd()
    47  		}
    48  		return "", fromproto.Error(err)
    49  	}
    50  	return resp.Path, err
    51  }
    52  
    53  // GetModulePath gets the current module path address.
    54  func (c *GRPCClient) GetModulePath() (addrs.Module, error) {
    55  	resp, err := c.Client.GetModulePath(context.Background(), &proto.GetModulePath_Request{})
    56  	if err != nil {
    57  		return nil, fromproto.Error(err)
    58  	}
    59  	return resp.Path, err
    60  }
    61  
    62  // GetResourceContent gets the contents of resources based on the schema.
    63  // This is shorthand of GetModuleContent for resources
    64  func (c *GRPCClient) GetResourceContent(name string, inner *hclext.BodySchema, opts *tflint.GetModuleContentOption) (*hclext.BodyContent, error) {
    65  	if opts == nil {
    66  		opts = &tflint.GetModuleContentOption{}
    67  	}
    68  	opts.Hint.ResourceType = name
    69  
    70  	body, err := c.GetModuleContent(&hclext.BodySchema{
    71  		Blocks: []hclext.BlockSchema{
    72  			{Type: "resource", LabelNames: []string{"type", "name"}, Body: inner},
    73  		},
    74  	}, opts)
    75  	if err != nil {
    76  		return nil, err
    77  	}
    78  
    79  	content := &hclext.BodyContent{Blocks: []*hclext.Block{}}
    80  	for _, resource := range body.Blocks {
    81  		if resource.Labels[0] != name {
    82  			continue
    83  		}
    84  
    85  		content.Blocks = append(content.Blocks, resource)
    86  	}
    87  
    88  	return content, nil
    89  }
    90  
    91  // GetProviderContent gets the contents of providers based on the schema.
    92  // This is shorthand of GetModuleContent for providers
    93  func (c *GRPCClient) GetProviderContent(name string, inner *hclext.BodySchema, opts *tflint.GetModuleContentOption) (*hclext.BodyContent, error) {
    94  	if opts == nil {
    95  		opts = &tflint.GetModuleContentOption{}
    96  	}
    97  
    98  	body, err := c.GetModuleContent(&hclext.BodySchema{
    99  		Blocks: []hclext.BlockSchema{
   100  			{Type: "provider", LabelNames: []string{"name"}, Body: inner},
   101  		},
   102  	}, opts)
   103  	if err != nil {
   104  		return nil, err
   105  	}
   106  
   107  	content := &hclext.BodyContent{Blocks: []*hclext.Block{}}
   108  	for _, provider := range body.Blocks {
   109  		if provider.Labels[0] != name {
   110  			continue
   111  		}
   112  
   113  		content.Blocks = append(content.Blocks, provider)
   114  	}
   115  
   116  	return content, nil
   117  }
   118  
   119  // GetModuleContent gets the contents of the module based on the schema.
   120  func (c *GRPCClient) GetModuleContent(schema *hclext.BodySchema, opts *tflint.GetModuleContentOption) (*hclext.BodyContent, error) {
   121  	if opts == nil {
   122  		opts = &tflint.GetModuleContentOption{}
   123  	}
   124  
   125  	req := &proto.GetModuleContent_Request{
   126  		Schema: toproto.BodySchema(schema),
   127  		Option: toproto.GetModuleContentOption(opts),
   128  	}
   129  	resp, err := c.Client.GetModuleContent(context.Background(), req)
   130  	if err != nil {
   131  		return nil, fromproto.Error(err)
   132  	}
   133  
   134  	body, diags := fromproto.BodyContent(resp.Content)
   135  	if diags.HasErrors() {
   136  		err = diags
   137  	}
   138  	return body, err
   139  }
   140  
   141  // GetFile returns hcl.File based on the passed file name.
   142  func (c *GRPCClient) GetFile(file string) (*hcl.File, error) {
   143  	resp, err := c.Client.GetFile(context.Background(), &proto.GetFile_Request{Name: file})
   144  	if err != nil {
   145  		return nil, fromproto.Error(err)
   146  	}
   147  
   148  	var f *hcl.File
   149  	var diags hcl.Diagnostics
   150  	if strings.HasSuffix(file, ".tf") {
   151  		f, diags = hclsyntax.ParseConfig(resp.File, file, hcl.InitialPos)
   152  	} else {
   153  		f, diags = hcljson.Parse(resp.File, file)
   154  	}
   155  
   156  	if diags.HasErrors() {
   157  		err = diags
   158  	}
   159  	return f, err
   160  }
   161  
   162  // GetFiles returns bytes of hcl.File in the self module context.
   163  func (c *GRPCClient) GetFiles() (map[string]*hcl.File, error) {
   164  	resp, err := c.Client.GetFiles(context.Background(), &proto.GetFiles_Request{})
   165  	if err != nil {
   166  		return nil, fromproto.Error(err)
   167  	}
   168  
   169  	files := map[string]*hcl.File{}
   170  	var f *hcl.File
   171  	var diags hcl.Diagnostics
   172  	for name, bytes := range resp.Files {
   173  		var d hcl.Diagnostics
   174  		if strings.HasSuffix(name, ".tf") {
   175  			f, d = hclsyntax.ParseConfig(bytes, name, hcl.InitialPos)
   176  		} else {
   177  			f, d = hcljson.Parse(bytes, name)
   178  		}
   179  		diags = diags.Extend(d)
   180  
   181  		files[name] = f
   182  	}
   183  
   184  	if diags.HasErrors() {
   185  		return files, diags
   186  	}
   187  	return files, nil
   188  }
   189  
   190  type nativeWalker struct {
   191  	walker tflint.ExprWalker
   192  }
   193  
   194  func (w *nativeWalker) Enter(node hclsyntax.Node) hcl.Diagnostics {
   195  	if expr, ok := node.(hcl.Expression); ok {
   196  		return w.walker.Enter(expr)
   197  	}
   198  	return nil
   199  }
   200  
   201  func (w *nativeWalker) Exit(node hclsyntax.Node) hcl.Diagnostics {
   202  	if expr, ok := node.(hcl.Expression); ok {
   203  		return w.walker.Exit(expr)
   204  	}
   205  	return nil
   206  }
   207  
   208  // WalkExpressions traverses expressions in all files by the passed walker.
   209  // Note that it behaves differently in native HCL syntax and JSON syntax.
   210  //
   211  // In the HCL syntax, `var.foo` and `var.bar` in `[var.foo, var.bar]` are
   212  // also passed to the walker. In other words, it traverses expressions recursively.
   213  // To avoid redundant checks, the walker should check the kind of expression.
   214  //
   215  // In the JSON syntax, only an expression of an attribute seen from the top
   216  // level of the file is passed. In other words, it doesn't traverse expressions
   217  // recursively. This is a limitation of JSON syntax.
   218  func (c *GRPCClient) WalkExpressions(walker tflint.ExprWalker) hcl.Diagnostics {
   219  	files, err := c.GetFiles()
   220  	if err != nil {
   221  		return hcl.Diagnostics{
   222  			{
   223  				Severity: hcl.DiagError,
   224  				Summary:  "failed to call GetFiles()",
   225  				Detail:   err.Error(),
   226  			},
   227  		}
   228  	}
   229  
   230  	diags := hcl.Diagnostics{}
   231  	for _, file := range files {
   232  		if body, ok := file.Body.(*hclsyntax.Body); ok {
   233  			walkDiags := hclsyntax.Walk(body, &nativeWalker{walker: walker})
   234  			diags = diags.Extend(walkDiags)
   235  			continue
   236  		}
   237  
   238  		// In JSON syntax, everything can be walked as an attribute.
   239  		attrs, jsonDiags := file.Body.JustAttributes()
   240  		if jsonDiags.HasErrors() {
   241  			diags = diags.Extend(jsonDiags)
   242  			continue
   243  		}
   244  
   245  		for _, attr := range attrs {
   246  			enterDiags := walker.Enter(attr.Expr)
   247  			diags = diags.Extend(enterDiags)
   248  			exitDiags := walker.Exit(attr.Expr)
   249  			diags = diags.Extend(exitDiags)
   250  		}
   251  	}
   252  
   253  	return diags
   254  }
   255  
   256  // DecodeRuleConfig guesses the schema of the rule config from the passed interface and sends the schema to GRPC server.
   257  // Content retrieved based on the schema is decoded into the passed interface.
   258  func (c *GRPCClient) DecodeRuleConfig(name string, ret interface{}) error {
   259  	resp, err := c.Client.GetRuleConfigContent(context.Background(), &proto.GetRuleConfigContent_Request{
   260  		Name:   name,
   261  		Schema: toproto.BodySchema(hclext.ImpliedBodySchema(ret)),
   262  	})
   263  	if err != nil {
   264  		return fromproto.Error(err)
   265  	}
   266  
   267  	content, diags := fromproto.BodyContent(resp.Content)
   268  	if diags.HasErrors() {
   269  		return diags
   270  	}
   271  	if content.IsEmpty() {
   272  		return nil
   273  	}
   274  
   275  	diags = hclext.DecodeBody(content, nil, ret)
   276  	if diags.HasErrors() {
   277  		return diags
   278  	}
   279  	return nil
   280  }
   281  
   282  var errRefTy = reflect.TypeOf((*error)(nil)).Elem()
   283  
   284  // EvaluateExpr evals the passed expression based on the type.
   285  // Passing a callback function instead of a value as the target will invoke the callback,
   286  // passing the evaluated value to the argument.
   287  func (c *GRPCClient) EvaluateExpr(expr hcl.Expression, target interface{}, opts *tflint.EvaluateExprOption) error {
   288  	rval := reflect.ValueOf(target)
   289  	rty := rval.Type()
   290  
   291  	var callback bool
   292  	switch rty.Kind() {
   293  	case reflect.Func:
   294  		// Callback must meet the following requirements:
   295  		//   - It must be a function
   296  		//   - It must take an argument
   297  		//   - It must return an error
   298  		if !(rty.NumIn() == 1 && rty.NumOut() == 1 && rty.Out(0).Implements(errRefTy)) {
   299  			panic(`callback must be of type "func (v T) error"`)
   300  		}
   301  		callback = true
   302  		target = reflect.New(rty.In(0)).Interface()
   303  
   304  	case reflect.Pointer:
   305  		// ok
   306  	default:
   307  		panic("target value is not a pointer or function")
   308  	}
   309  
   310  	err := c.evaluateExpr(expr, target, opts)
   311  	if !callback {
   312  		// error should be handled in the caller
   313  		return err
   314  	}
   315  
   316  	if err != nil {
   317  		// If it cannot be represented as a Go value, exit without invoking the callback rather than returning an error.
   318  		if errors.Is(err, tflint.ErrUnknownValue) ||
   319  			errors.Is(err, tflint.ErrNullValue) ||
   320  			errors.Is(err, tflint.ErrSensitive) ||
   321  			errors.Is(err, tflint.ErrEphemeral) ||
   322  			errors.Is(err, tflint.ErrUnevaluable) {
   323  			return nil
   324  		}
   325  		return err
   326  	}
   327  
   328  	rerr := rval.Call([]reflect.Value{reflect.ValueOf(target).Elem()})
   329  	if rerr[0].IsNil() {
   330  		return nil
   331  	}
   332  	return rerr[0].Interface().(error)
   333  }
   334  
   335  func (c *GRPCClient) evaluateExpr(expr hcl.Expression, target interface{}, opts *tflint.EvaluateExprOption) error {
   336  	if opts == nil {
   337  		opts = &tflint.EvaluateExprOption{}
   338  	}
   339  
   340  	var ty cty.Type
   341  	if opts.WantType != nil {
   342  		ty = *opts.WantType
   343  	} else {
   344  		switch target.(type) {
   345  		case *string:
   346  			ty = cty.String
   347  		case *int:
   348  			ty = cty.Number
   349  		case *bool:
   350  			ty = cty.Bool
   351  		case *[]string:
   352  			ty = cty.List(cty.String)
   353  		case *[]int:
   354  			ty = cty.List(cty.Number)
   355  		case *[]bool:
   356  			ty = cty.List(cty.Bool)
   357  		case *map[string]string:
   358  			ty = cty.Map(cty.String)
   359  		case *map[string]int:
   360  			ty = cty.Map(cty.Number)
   361  		case *map[string]bool:
   362  			ty = cty.Map(cty.Bool)
   363  		case *cty.Value:
   364  			ty = cty.DynamicPseudoType
   365  		default:
   366  			panic(fmt.Sprintf("unsupported target type: %T", target))
   367  		}
   368  	}
   369  	tyby, err := json.MarshalType(ty)
   370  	if err != nil {
   371  		return err
   372  	}
   373  
   374  	file, err := c.GetFile(expr.Range().Filename)
   375  	if err != nil {
   376  		return err
   377  	}
   378  
   379  	resp, err := c.Client.EvaluateExpr(
   380  		context.Background(),
   381  		&proto.EvaluateExpr_Request{
   382  			Expression: toproto.Expression(expr, file.Bytes),
   383  			Option:     &proto.EvaluateExpr_Option{Type: tyby, ModuleCtx: toproto.ModuleCtxType(opts.ModuleCtx)},
   384  		},
   385  	)
   386  	if err != nil {
   387  		return fromproto.Error(err)
   388  	}
   389  
   390  	val, err := fromproto.Value(resp.Value, ty, resp.Marks)
   391  	if err != nil {
   392  		return err
   393  	}
   394  
   395  	if ty == cty.DynamicPseudoType {
   396  		return gocty.FromCtyValue(val, target)
   397  	}
   398  
   399  	// Returns an error if the value cannot be decoded to a Go value (e.g. unknown, null, marked).
   400  	// This allows the caller to handle the value by the errors package.
   401  	err = cty.Walk(val, func(path cty.Path, v cty.Value) (bool, error) {
   402  		if !v.IsKnown() {
   403  			logger.Debug(fmt.Sprintf("unknown value found in %s", expr.Range()))
   404  			return false, tflint.ErrUnknownValue
   405  		}
   406  		if v.IsNull() {
   407  			logger.Debug(fmt.Sprintf("null value found in %s", expr.Range()))
   408  			return false, tflint.ErrNullValue
   409  		}
   410  		if v.HasMark(marks.Sensitive) {
   411  			logger.Debug(fmt.Sprintf("sensitive value found in %s", expr.Range()))
   412  			return false, tflint.ErrSensitive
   413  		}
   414  		if v.HasMark(marks.Ephemeral) {
   415  			logger.Debug(fmt.Sprintf("ephemeral value found in %s", expr.Range()))
   416  			return false, tflint.ErrEphemeral
   417  		}
   418  		return true, nil
   419  	})
   420  	if err != nil {
   421  		return err
   422  	}
   423  
   424  	return gocty.FromCtyValue(val, target)
   425  }
   426  
   427  // EmitIssue emits the issue with the passed rule, message, location
   428  func (c *GRPCClient) EmitIssue(rule tflint.Rule, message string, location hcl.Range) error {
   429  	_, err := c.Client.EmitIssue(context.Background(), &proto.EmitIssue_Request{Rule: toproto.Rule(rule), Message: message, Range: toproto.Range(location)})
   430  	if err != nil {
   431  		return fromproto.Error(err)
   432  	}
   433  	return nil
   434  }
   435  
   436  // EmitIssueWithFix emits the issue with the passed rule, message, location.
   437  // Invoke the fix function and add the changes to the fixer.
   438  // If the fix function returns ErrFixNotSupported, the emitted issue will not
   439  // be marked as fixable.
   440  func (c *GRPCClient) EmitIssueWithFix(rule tflint.Rule, message string, location hcl.Range, fixFunc func(f tflint.Fixer) error) error {
   441  	var fixable bool
   442  	var fixErr error
   443  
   444  	path, err := c.GetModulePath()
   445  	if err != nil {
   446  		return fromproto.Error(err)
   447  	}
   448  	// If the issue is not in the root module, skip the fix.
   449  	if path.IsRoot() {
   450  		fixable = true
   451  		c.Fixer.StashChanges()
   452  
   453  		fixErr = fixFunc(c.Fixer)
   454  		if errors.Is(fixErr, tflint.ErrFixNotSupported) {
   455  			fixable = false
   456  		}
   457  	}
   458  
   459  	resp, err := c.Client.EmitIssue(context.Background(), &proto.EmitIssue_Request{Rule: toproto.Rule(rule), Message: message, Range: toproto.Range(location), Fixable: fixable})
   460  	if err != nil {
   461  		return fromproto.Error(err)
   462  	}
   463  
   464  	if !c.FixEnabled || !fixable || !resp.Applied {
   465  		c.Fixer.PopChangesFromStash()
   466  		return nil
   467  	}
   468  	return fixErr
   469  }
   470  
   471  // ApplyChanges applies the changes in the fixer to the server
   472  func (c *GRPCClient) ApplyChanges() error {
   473  	_, err := c.Client.ApplyChanges(context.Background(), &proto.ApplyChanges_Request{Changes: c.Fixer.Changes()})
   474  	if err != nil {
   475  		return fromproto.Error(err)
   476  	}
   477  	c.Fixer.ApplyChanges()
   478  	return nil
   479  }
   480  
   481  // EnsureNoError is a helper for error handling. Depending on the type of error generated by EvaluateExpr,
   482  // determine whether to exit, skip, or continue. If it is continued, the passed function will be executed.
   483  //
   484  // Deprecated: Use errors.Is() instead to determine which errors can be ignored.
   485  func (*GRPCClient) EnsureNoError(err error, proc func() error) error {
   486  	if err == nil {
   487  		return proc()
   488  	}
   489  
   490  	if errors.Is(err, tflint.ErrUnevaluable) || errors.Is(err, tflint.ErrNullValue) || errors.Is(err, tflint.ErrUnknownValue) || errors.Is(err, tflint.ErrSensitive) {
   491  		return nil
   492  	}
   493  	return err
   494  }