diff --git a/cmd/inspect.go b/cmd/inspect.go index c13ee8970..653f00f06 100644 --- a/cmd/inspect.go +++ b/cmd/inspect.go @@ -188,9 +188,19 @@ func (cli *CLI) inspectModule(opts Options, dir string, filterFiles []string) (t } // Run inspection - for _, ruleset := range rulesetPlugin.RuleSets { + for name, ruleset := range rulesetPlugin.RuleSets { + sdkVersion, err := ruleset.SDKVersion() + if err != nil { + if st, ok := status.FromError(err); ok && st.Code() == codes.Unimplemented { + // SDKVersion endpoint is available in tflint-plugin-sdk v0.14+. + // Use nil if not available. + } else { + return tflint.Issues{}, fmt.Errorf("Failed to get TFLint version constraints to `%s` plugin; %w", name, err) + } + } + for _, runner := range runners { - err = ruleset.Check(plugin.NewGRPCServer(runner, rootRunner, cli.loader.Files())) + err = ruleset.Check(plugin.NewGRPCServer(runner, rootRunner, cli.loader.Files(), sdkVersion)) if err != nil { return tflint.Issues{}, fmt.Errorf("Failed to check ruleset; %w", err) } diff --git a/go.mod b/go.mod index 26afb436c..58513ee4d 100644 --- a/go.mod +++ b/go.mod @@ -24,7 +24,7 @@ require ( github.com/sourcegraph/go-lsp v0.0.0-20200429204803-219e11d77f5d github.com/sourcegraph/jsonrpc2 v0.1.0 github.com/spf13/afero v1.9.3 - github.com/terraform-linters/tflint-plugin-sdk v0.15.0 + github.com/terraform-linters/tflint-plugin-sdk v0.15.1-0.20230225141907-dd804b3671af github.com/terraform-linters/tflint-ruleset-terraform v0.2.2 github.com/xeipuuv/gojsonschema v1.2.0 github.com/zclconf/go-cty v1.12.1 diff --git a/go.sum b/go.sum index 3092d84b6..639a87286 100644 --- a/go.sum +++ b/go.sum @@ -457,8 +457,8 @@ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1F github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/terraform-linters/tflint-plugin-sdk v0.15.0 h1:bUJ9OskzT/I98XaJ5+rs7ymVPHiGT8oI4bG86LkopVY= -github.com/terraform-linters/tflint-plugin-sdk v0.15.0/go.mod h1:enH5i7SHelcvC2AGZavEJzcrRF7nhAaOwTdaBjr/Zjo= +github.com/terraform-linters/tflint-plugin-sdk v0.15.1-0.20230225141907-dd804b3671af h1:TAsqOUKu3DXg6ZmV3igB8ksKkHkaQrdSdZfCE3Ff7nc= +github.com/terraform-linters/tflint-plugin-sdk v0.15.1-0.20230225141907-dd804b3671af/go.mod h1:g5UIXcskejxp38JWqvYqEb/HkvIX6X6luEdS60yimTw= github.com/terraform-linters/tflint-ruleset-terraform v0.2.2 h1:iTE09KkaZ0DE29xvp6IIM1/gmas9V0h8CER28SyBmQ8= github.com/terraform-linters/tflint-ruleset-terraform v0.2.2/go.mod h1:bCkvH8Vqzr16bWEE3e6Q3hvdZlmSAOR8i6G3M5y+M+k= github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8= diff --git a/langserver/handler.go b/langserver/handler.go index bb185170c..daf79f659 100644 --- a/langserver/handler.go +++ b/langserver/handler.go @@ -11,6 +11,7 @@ import ( "runtime" "strings" + "github.com/hashicorp/go-version" "github.com/hashicorp/hcl/v2" lsp "github.com/sourcegraph/go-lsp" "github.com/sourcegraph/jsonrpc2" @@ -43,6 +44,7 @@ func NewHandler(configPath string, cliConfig *tflint.Config) (jsonrpc2.Handler, } rulesets := []tflint.RuleSet{} + clientSDKVersions := map[string]*version.Version{} for name, ruleset := range rulsetPlugin.RuleSets { constraints, err := ruleset.VersionConstraints() if err != nil { @@ -56,6 +58,17 @@ func NewHandler(configPath string, cliConfig *tflint.Config) (jsonrpc2.Handler, if !constraints.Check(tflint.Version) { return nil, nil, fmt.Errorf("Failed to satisfy version constraints; tflint-ruleset-%s requires %s, but TFLint version is %s", name, constraints, tflint.Version) } + + clientSDKVersions[name], err = ruleset.SDKVersion() + if err != nil { + if st, ok := status.FromError(err); ok && st.Code() == codes.Unimplemented { + // SDKVersion endpoint is available in tflint-plugin-sdk v0.14+. + // Use nil if not available. + } else { + return nil, nil, fmt.Errorf("Failed to get SDK version of `%s` plugin; %w", name, err) + } + } + rulesets = append(rulesets, ruleset) } if err := cliConfig.ValidateRules(rulesets...); err != nil { @@ -63,24 +76,26 @@ func NewHandler(configPath string, cliConfig *tflint.Config) (jsonrpc2.Handler, } return jsonrpc2.HandlerWithError((&handler{ - configPath: configPath, - cliConfig: cliConfig, - config: cfg, - fs: afero.NewCopyOnWriteFs(afero.NewOsFs(), afero.NewMemMapFs()), - plugin: rulsetPlugin, - diagsPaths: []string{}, + configPath: configPath, + cliConfig: cliConfig, + config: cfg, + fs: afero.NewCopyOnWriteFs(afero.NewOsFs(), afero.NewMemMapFs()), + plugin: rulsetPlugin, + clientSDKVersions: clientSDKVersions, + diagsPaths: []string{}, }).handle), rulsetPlugin, nil } type handler struct { - configPath string - cliConfig *tflint.Config - config *tflint.Config - fs afero.Fs - rootDir string - plugin *plugin.Plugin - shutdown bool - diagsPaths []string + configPath string + cliConfig *tflint.Config + config *tflint.Config + fs afero.Fs + rootDir string + plugin *plugin.Plugin + clientSDKVersions map[string]*version.Version + shutdown bool + diagsPaths []string } func (h *handler) handle(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) (result interface{}, err error) { @@ -210,7 +225,7 @@ func (h *handler) inspect() (map[string][]lsp.Diagnostic, error) { return ret, fmt.Errorf("Failed to apply config to `%s` plugin", name) } for _, runner := range runners { - err = ruleset.Check(plugin.NewGRPCServer(runner, runners[len(runners)-1], loader.Files())) + err = ruleset.Check(plugin.NewGRPCServer(runner, runners[len(runners)-1], loader.Files(), h.clientSDKVersions[name])) if err != nil { return ret, fmt.Errorf("Failed to check ruleset: %w", err) } diff --git a/plugin/server.go b/plugin/server.go index 7af8c4e91..7793a74b0 100644 --- a/plugin/server.go +++ b/plugin/server.go @@ -5,6 +5,7 @@ import ( "fmt" "log" + "github.com/hashicorp/go-version" hcl "github.com/hashicorp/hcl/v2" "github.com/terraform-linters/tflint-plugin-sdk/hclext" sdk "github.com/terraform-linters/tflint-plugin-sdk/tflint" @@ -15,14 +16,15 @@ import ( // GRPCServer is a gRPC server for responding to requests from plugins. type GRPCServer struct { - runner *tflint.Runner - rootRunner *tflint.Runner - files map[string]*hcl.File + runner *tflint.Runner + rootRunner *tflint.Runner + files map[string]*hcl.File + clientSDKVersion *version.Version } // NewGRPCServer initializes a gRPC server for plugins. -func NewGRPCServer(runner *tflint.Runner, rootRunner *tflint.Runner, files map[string]*hcl.File) *GRPCServer { - return &GRPCServer{runner: runner, rootRunner: rootRunner, files: files} +func NewGRPCServer(runner *tflint.Runner, rootRunner *tflint.Runner, files map[string]*hcl.File, sdkVersion *version.Version) *GRPCServer { + return &GRPCServer{runner: runner, rootRunner: rootRunner, files: files, clientSDKVersion: sdkVersion} } // GetOriginalwd returns the original working directory. @@ -136,6 +138,11 @@ func (s *GRPCServer) EvaluateExpr(expr hcl.Expression, opts sdk.EvaluateExprOpti return cty.NullVal(cty.NilType), err } + // SDK v0.16+ introduces client-side handling of unknown and NULL values. + if s.clientSDKVersion != nil && s.clientSDKVersion.GreaterThanOrEqual(version.Must(version.NewVersion("0.16.0"))) { + return val, nil + } + if *opts.WantType == cty.DynamicPseudoType { return val, nil } diff --git a/plugin/server_test.go b/plugin/server_test.go index e64d5b5de..8220df444 100644 --- a/plugin/server_test.go +++ b/plugin/server_test.go @@ -8,15 +8,19 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/hashicorp/go-version" hcl "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/spf13/afero" "github.com/terraform-linters/tflint-plugin-sdk/hclext" + "github.com/terraform-linters/tflint-plugin-sdk/plugin/host2plugin" sdk "github.com/terraform-linters/tflint-plugin-sdk/tflint" "github.com/terraform-linters/tflint/tflint" "github.com/zclconf/go-cty/cty" ) +var SDKVersion = version.Must(version.NewVersion(host2plugin.SDKVersion)) + func TestGetModuleContent(t *testing.T) { runner := tflint.TestRunner(t, map[string]string{"main.tf": ` resource "aws_instance" "foo" { @@ -43,7 +47,7 @@ resource "aws_instance" "bar" { instance_type = "m5.2xlarge" }`}) - server := NewGRPCServer(runner, rootRunner, runner.Files()) + server := NewGRPCServer(runner, rootRunner, runner.Files(), SDKVersion) tests := []struct { Name string @@ -257,7 +261,7 @@ resource "aws_instance" "foo" { files[name] = file } - server := NewGRPCServer(runner, rootRunner, files) + server := NewGRPCServer(runner, rootRunner, files, SDKVersion) tests := []struct { Name string @@ -324,7 +328,7 @@ resource "aws_instance" "bar" { instance_type = "m5.2xlarge" }`}) - server := NewGRPCServer(runner, rootRunner, runner.Files()) + server := NewGRPCServer(runner, rootRunner, runner.Files(), SDKVersion) tests := []struct { Name string @@ -388,7 +392,7 @@ rule "test_in_file" { fileConfig.Merge(cliConfig) runner := tflint.TestRunnerWithConfig(t, map[string]string{}, fileConfig) - server := NewGRPCServer(runner, nil, runner.Files()) + server := NewGRPCServer(runner, nil, runner.Files(), SDKVersion) // default error check helper neverHappend := func(err error) bool { return err != nil } @@ -492,7 +496,9 @@ variable "foo" { default = "baz" }`}) - server := NewGRPCServer(runner, rootRunner, runner.Files()) + server := NewGRPCServer(runner, rootRunner, runner.Files(), SDKVersion) + + sdkv15 := version.Must(version.NewVersion("0.15.0")) // test util functions hclExpr := func(expr string) hcl.Expression { @@ -510,10 +516,11 @@ variable "foo" { neverHappend := func(err error) bool { return err != nil } tests := []struct { - Name string - Args func() (hcl.Expression, sdk.EvaluateExprOption) - Want cty.Value - ErrCheck func(error) bool + Name string + Args func() (hcl.Expression, sdk.EvaluateExprOption) + SDKVersion *version.Version + Want cty.Value + ErrCheck func(error) bool }{ { Name: "self module context", @@ -557,26 +564,37 @@ variable "foo" { Args: func() (hcl.Expression, sdk.EvaluateExprOption) { return hclExpr(`var.no_default`), sdk.EvaluateExprOption{WantType: &cty.String, ModuleCtx: sdk.SelfModuleCtxType} }, - Want: cty.NullVal(cty.NilType), + Want: cty.UnknownVal(cty.String), + ErrCheck: neverHappend, + }, + { + Name: "no default (SDK v0.15)", + Args: func() (hcl.Expression, sdk.EvaluateExprOption) { + return hclExpr(`var.no_default`), sdk.EvaluateExprOption{WantType: &cty.String, ModuleCtx: sdk.SelfModuleCtxType} + }, + SDKVersion: sdkv15, + Want: cty.NullVal(cty.NilType), ErrCheck: func(err error) bool { return err == nil || !errors.Is(err, sdk.ErrUnknownValue) }, }, { - Name: "no default as cty.Value", + Name: "no default as cty.Value (SDK v0.15)", Args: func() (hcl.Expression, sdk.EvaluateExprOption) { return hclExpr(`var.no_default`), sdk.EvaluateExprOption{WantType: &cty.DynamicPseudoType, ModuleCtx: sdk.SelfModuleCtxType} }, - Want: cty.DynamicVal, - ErrCheck: neverHappend, + SDKVersion: sdkv15, + Want: cty.DynamicVal, + ErrCheck: neverHappend, }, { - Name: "no default value in object", + Name: "no default value in object (SDK v0.15)", Args: func() (hcl.Expression, sdk.EvaluateExprOption) { ty := cty.Object(map[string]cty.Type{"value": cty.String}) return hclExpr(`{ value = var.no_default }`), sdk.EvaluateExprOption{WantType: &ty, ModuleCtx: sdk.SelfModuleCtxType} }, - Want: cty.NullVal(cty.NilType), + SDKVersion: sdkv15, + Want: cty.NullVal(cty.NilType), ErrCheck: func(err error) bool { return err == nil || !errors.Is(err, sdk.ErrUnknownValue) }, @@ -586,63 +604,50 @@ variable "foo" { Args: func() (hcl.Expression, sdk.EvaluateExprOption) { return hclExpr(`var.null`), sdk.EvaluateExprOption{WantType: &cty.String, ModuleCtx: sdk.SelfModuleCtxType} }, - Want: cty.NullVal(cty.NilType), - ErrCheck: func(err error) bool { - return err == nil || !errors.Is(err, sdk.ErrNullValue) - }, - }, - { - Name: "null as cty.Value", - Args: func() (hcl.Expression, sdk.EvaluateExprOption) { - return hclExpr(`var.null`), sdk.EvaluateExprOption{WantType: &cty.DynamicPseudoType, ModuleCtx: sdk.SelfModuleCtxType} - }, Want: cty.NullVal(cty.String), ErrCheck: neverHappend, }, { - Name: "null value in object", + Name: "null (SDK v0.15)", Args: func() (hcl.Expression, sdk.EvaluateExprOption) { - ty := cty.Object(map[string]cty.Type{"value": cty.String}) - return hclExpr(`{ value = var.null }`), sdk.EvaluateExprOption{WantType: &ty, ModuleCtx: sdk.SelfModuleCtxType} + return hclExpr(`var.null`), sdk.EvaluateExprOption{WantType: &cty.String, ModuleCtx: sdk.SelfModuleCtxType} }, - Want: cty.NullVal(cty.NilType), + SDKVersion: sdkv15, + Want: cty.NullVal(cty.NilType), ErrCheck: func(err error) bool { return err == nil || !errors.Is(err, sdk.ErrNullValue) }, }, { - Name: "unevaluable", + Name: "null as cty.Value (SDK v0.15)", Args: func() (hcl.Expression, sdk.EvaluateExprOption) { - return hclExpr(`module.instance.output`), sdk.EvaluateExprOption{WantType: &cty.String, ModuleCtx: sdk.SelfModuleCtxType} - }, - Want: cty.NullVal(cty.NilType), - ErrCheck: func(err error) bool { - return err == nil || !errors.Is(err, sdk.ErrUnknownValue) - }, - }, - { - Name: "unevaluable as cty.Value", - Args: func() (hcl.Expression, sdk.EvaluateExprOption) { - return hclExpr(`module.instance.output`), sdk.EvaluateExprOption{WantType: &cty.DynamicPseudoType, ModuleCtx: sdk.SelfModuleCtxType} + return hclExpr(`var.null`), sdk.EvaluateExprOption{WantType: &cty.DynamicPseudoType, ModuleCtx: sdk.SelfModuleCtxType} }, - Want: cty.DynamicVal, - ErrCheck: neverHappend, + SDKVersion: sdkv15, + Want: cty.NullVal(cty.String), + ErrCheck: neverHappend, }, { - Name: "unevaluable value in object", + Name: "null value in object (SDK v0.15)", Args: func() (hcl.Expression, sdk.EvaluateExprOption) { ty := cty.Object(map[string]cty.Type{"value": cty.String}) - return hclExpr(`{ value = module.instance.output }`), sdk.EvaluateExprOption{WantType: &ty, ModuleCtx: sdk.SelfModuleCtxType} + return hclExpr(`{ value = var.null }`), sdk.EvaluateExprOption{WantType: &ty, ModuleCtx: sdk.SelfModuleCtxType} }, - Want: cty.NullVal(cty.NilType), + Want: cty.NullVal(cty.NilType), + SDKVersion: sdkv15, ErrCheck: func(err error) bool { - return err == nil || !errors.Is(err, sdk.ErrUnknownValue) + return err == nil || !errors.Is(err, sdk.ErrNullValue) }, }, } for _, test := range tests { t.Run(test.Name, func(t *testing.T) { + if test.SDKVersion == nil { + test.SDKVersion = SDKVersion + } + server.clientSDKVersion = test.SDKVersion + got, err := server.EvaluateExpr(test.Args()) if test.ErrCheck(err) { t.Fatalf("failed to call EvaluateExpr: %s", err) @@ -721,7 +726,7 @@ resource "aws_instance" "foo" { t.Run(test.Name, func(t *testing.T) { runner := tflint.TestRunner(t, map[string]string{"main.tf": config}) - server := NewGRPCServer(runner, nil, runner.Files()) + server := NewGRPCServer(runner, nil, runner.Files(), SDKVersion) err := server.EmitIssue(test.Args()) if err != nil {