Skip to content

Commit ced163f

Browse files
feat: Add SDK version checking for JSON config compatibility
- Extract SDK version checking into checkPluginSDKVersions function - Modify launchPlugins to return SDK versions for all plugins - Add warning when JSON config is used with plugins using SDK < 0.23.0 - Add IsJSONConfig() method to track when JSON configs are loaded - Add integration tests for JSON config (will pass once SDK 0.23.0 is released) - Keep support for any filename with --config option, not just .tflint.json
1 parent 29f637e commit ced163f

File tree

13 files changed

+223
-39
lines changed

13 files changed

+223
-39
lines changed

cmd/inspect.go

Lines changed: 59 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ func (cli *CLI) inspectModule(opts Options, dir string, filterFiles []string) (t
113113
}
114114

115115
// Launch plugin processes
116-
rulesetPlugin, err := launchPlugins(cli.config, opts.Fix)
116+
rulesetPlugin, sdkVersions, err := launchPlugins(cli.config, opts.Fix)
117117
if rulesetPlugin != nil {
118118
defer rulesetPlugin.Clean()
119119
go cli.registerShutdownHandler(func() {
@@ -125,22 +125,18 @@ func (cli *CLI) inspectModule(opts Options, dir string, filterFiles []string) (t
125125
return issues, changes, err
126126
}
127127

128-
// Check preconditions
129-
sdkVersions := map[string]*version.Version{}
130-
for name, ruleset := range rulesetPlugin.RuleSets {
131-
sdkVersion, err := ruleset.SDKVersion()
132-
if err != nil {
133-
if st, ok := status.FromError(err); ok && st.Code() == codes.Unimplemented {
134-
// SDKVersion endpoint is available in tflint-plugin-sdk v0.14+.
135-
return issues, changes, fmt.Errorf(`Plugin "%s" SDK version is incompatible. Compatible versions: %s`, name, plugin.SDKVersionConstraints)
136-
} else {
137-
return issues, changes, fmt.Errorf(`Failed to get plugin "%s" SDK version; %w`, name, err)
128+
// Check if we're using a JSON config file and if any plugins need SDK updates
129+
if cli.config.IsJSONConfig() && len(sdkVersions) > 0 {
130+
minVersion := version.Must(version.NewVersion("0.23.0"))
131+
hasIncompatible, incompatiblePlugins := checkPluginSDKVersions(sdkVersions, minVersion)
132+
if hasIncompatible {
133+
// Log warning for JSON config with older SDK versions
134+
fmt.Fprintf(cli.errStream, "WARNING: JSON configuration detected with plugin(s) using SDK < 0.23.0:\n")
135+
for _, plugin := range incompatiblePlugins {
136+
fmt.Fprintf(cli.errStream, " - %s\n", plugin)
138137
}
138+
fmt.Fprintf(cli.errStream, "Please update your plugins for full JSON configuration support.\n\n")
139139
}
140-
if !plugin.SDKVersionConstraints.Check(sdkVersion) {
141-
return issues, changes, fmt.Errorf(`Plugin "%s" SDK version (%s) is incompatible. Compatible versions: %s`, name, sdkVersion, plugin.SDKVersionConstraints)
142-
}
143-
sdkVersions[name] = sdkVersion
144140
}
145141

146142
// Run inspection
@@ -254,11 +250,32 @@ func (cli *CLI) setupRunners(opts Options, dir string) (*tflint.Runner, []*tflin
254250
return runner, moduleRunners, nil
255251
}
256252

257-
func launchPlugins(config *tflint.Config, fix bool) (*plugin.Plugin, error) {
253+
func launchPlugins(config *tflint.Config, fix bool) (*plugin.Plugin, map[string]*version.Version, error) {
258254
// Lookup plugins
259255
rulesetPlugin, err := plugin.Discovery(config)
260256
if err != nil {
261-
return nil, fmt.Errorf("Failed to initialize plugins; %w", err)
257+
return nil, nil, fmt.Errorf("Failed to initialize plugins; %w", err)
258+
}
259+
260+
// Collect SDK versions from all plugins
261+
sdkVersions := map[string]*version.Version{}
262+
for name, ruleset := range rulesetPlugin.RuleSets {
263+
sdkVersion, err := ruleset.SDKVersion()
264+
if err != nil {
265+
if st, ok := status.FromError(err); ok && st.Code() == codes.Unimplemented {
266+
// SDKVersion endpoint is available in tflint-plugin-sdk v0.14+.
267+
// Assume old version for compatibility checking
268+
sdkVersions[name] = version.Must(version.NewVersion("0.13.0"))
269+
} else {
270+
return nil, nil, fmt.Errorf(`Failed to get plugin "%s" SDK version; %w`, name, err)
271+
}
272+
} else {
273+
// Check SDK version compatibility with TFLint
274+
if !plugin.SDKVersionConstraints.Check(sdkVersion) {
275+
return nil, nil, fmt.Errorf(`Plugin "%s" SDK version (%s) is incompatible. Compatible versions: %s`, name, sdkVersion, plugin.SDKVersionConstraints)
276+
}
277+
sdkVersions[name] = sdkVersion
278+
}
262279
}
263280

264281
rulesets := []tflint.RuleSet{}
@@ -271,44 +288,44 @@ func launchPlugins(config *tflint.Config, fix bool) (*plugin.Plugin, error) {
271288
if err != nil {
272289
if st, ok := status.FromError(err); ok && st.Code() == codes.Unimplemented {
273290
// VersionConstraints endpoint is available in tflint-plugin-sdk v0.14+.
274-
return rulesetPlugin, fmt.Errorf(`Plugin "%s" SDK version is incompatible. Compatible versions: %s`, name, plugin.SDKVersionConstraints)
291+
return rulesetPlugin, sdkVersions, fmt.Errorf(`Plugin "%s" SDK version is incompatible. Compatible versions: %s`, name, plugin.SDKVersionConstraints)
275292
} else {
276-
return rulesetPlugin, fmt.Errorf(`Failed to get TFLint version constraints to "%s" plugin; %w`, name, err)
293+
return rulesetPlugin, sdkVersions, fmt.Errorf(`Failed to get TFLint version constraints to "%s" plugin; %w`, name, err)
277294
}
278295
}
279296
if !constraints.Check(tflint.Version) {
280-
return rulesetPlugin, fmt.Errorf("Failed to satisfy version constraints; tflint-ruleset-%s requires %s, but TFLint version is %s", name, constraints, tflint.Version)
297+
return rulesetPlugin, sdkVersions, fmt.Errorf("Failed to satisfy version constraints; tflint-ruleset-%s requires %s, but TFLint version is %s", name, constraints, tflint.Version)
281298
}
282299

283300
if err := ruleset.ApplyGlobalConfig(pluginConf); err != nil {
284-
return rulesetPlugin, fmt.Errorf(`Failed to apply global config to "%s" plugin; %w`, name, err)
301+
return rulesetPlugin, sdkVersions, fmt.Errorf(`Failed to apply global config to "%s" plugin; %w`, name, err)
285302
}
286303
configSchema, err := ruleset.ConfigSchema()
287304
if err != nil {
288-
return rulesetPlugin, fmt.Errorf(`Failed to fetch config schema from "%s" plugin; %w`, name, err)
305+
return rulesetPlugin, sdkVersions, fmt.Errorf(`Failed to fetch config schema from "%s" plugin; %w`, name, err)
289306
}
290307
content := &hclext.BodyContent{}
291308
if plugin, exists := config.Plugins[name]; exists {
292309
var diags hcl.Diagnostics
293310
content, diags = plugin.Content(configSchema)
294311
if diags.HasErrors() {
295-
return rulesetPlugin, fmt.Errorf(`Failed to parse "%s" plugin config; %w`, name, diags)
312+
return rulesetPlugin, sdkVersions, fmt.Errorf(`Failed to parse "%s" plugin config; %w`, name, diags)
296313
}
297314
}
298315
err = ruleset.ApplyConfig(content, config.Sources())
299316
if err != nil {
300-
return rulesetPlugin, fmt.Errorf(`Failed to apply config to "%s" plugin; %w`, name, err)
317+
return rulesetPlugin, sdkVersions, fmt.Errorf(`Failed to apply config to "%s" plugin; %w`, name, err)
301318
}
302319

303320
rulesets = append(rulesets, ruleset)
304321
}
305322

306323
// Validate config for plugins
307324
if err := config.ValidateRules(rulesets...); err != nil {
308-
return rulesetPlugin, fmt.Errorf("Failed to check rule config; %w", err)
325+
return rulesetPlugin, sdkVersions, fmt.Errorf("Failed to check rule config; %w", err)
309326
}
310327

311-
return rulesetPlugin, nil
328+
return rulesetPlugin, sdkVersions, nil
312329
}
313330

314331
func writeChanges(changes map[string][]byte) error {
@@ -334,6 +351,22 @@ func writeChanges(changes map[string][]byte) error {
334351
}
335352

336353
// Checks if the given issues contain severities above or equal to the given minimum failure opt. Defaults to true if an error occurs
354+
// checkPluginSDKVersions checks if plugin SDK versions meet the minimum requirement
355+
// Returns true if any plugins are below the minimum version, along with a list of incompatible plugins
356+
func checkPluginSDKVersions(sdkVersions map[string]*version.Version, minVersion *version.Version) (bool, []string) {
357+
hasIncompatiblePlugins := false
358+
incompatiblePlugins := []string{}
359+
360+
for name, sdkVersion := range sdkVersions {
361+
if sdkVersion.LessThan(minVersion) {
362+
hasIncompatiblePlugins = true
363+
incompatiblePlugins = append(incompatiblePlugins, fmt.Sprintf("%s (v%s)", name, sdkVersion))
364+
}
365+
}
366+
367+
return hasIncompatiblePlugins, incompatiblePlugins
368+
}
369+
337370
func exceedsMinimumFailure(issues tflint.Issues, minimumFailureOpt string) bool {
338371
if minimumFailureOpt != "" {
339372
minSeverity, err := tflint.NewSeverity(minimumFailureOpt)

go.mod

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ require (
2525
github.com/sourcegraph/go-lsp v0.0.0-20200429204803-219e11d77f5d
2626
github.com/sourcegraph/jsonrpc2 v0.2.1
2727
github.com/spf13/afero v1.15.0
28-
github.com/terraform-linters/tflint-plugin-sdk v0.22.0
28+
github.com/terraform-linters/tflint-plugin-sdk v0.23.0
2929
github.com/terraform-linters/tflint-ruleset-terraform v0.13.0
3030
github.com/xeipuuv/gojsonschema v1.2.0
3131
github.com/zclconf/go-cty v1.17.0
@@ -176,17 +176,17 @@ require (
176176
golang.org/x/mod v0.28.0 // indirect
177177
golang.org/x/sync v0.17.0 // indirect
178178
golang.org/x/sys v0.36.0 // indirect
179-
golang.org/x/telemetry v0.0.0-20250807160809-1a19826ec488 // indirect
179+
golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053 // indirect
180180
golang.org/x/term v0.35.0 // indirect
181181
golang.org/x/time v0.12.0 // indirect
182-
golang.org/x/tools v0.36.0 // indirect
182+
golang.org/x/tools v0.37.0 // indirect
183183
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated // indirect
184184
golang.org/x/vuln v1.1.4 // indirect
185185
google.golang.org/api v0.248.0 // indirect
186186
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect
187187
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c // indirect
188188
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c // indirect
189-
google.golang.org/protobuf v1.36.8 // indirect
189+
google.golang.org/protobuf v1.36.9 // indirect
190190
gopkg.in/yaml.v3 v3.0.1 // indirect
191191
k8s.io/klog/v2 v2.130.1 // indirect
192192
)

go.sum

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1288,8 +1288,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
12881288
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
12891289
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
12901290
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
1291-
github.com/terraform-linters/tflint-plugin-sdk v0.22.0 h1:holOVJW0hjf0wkjtnYyPWRooQNp8ETUcKE86rdYkH5U=
1292-
github.com/terraform-linters/tflint-plugin-sdk v0.22.0/go.mod h1:Cag3YJjBpHdQzI/limZR+Cj7WYPLTIE61xsCdIXoeUI=
1291+
github.com/terraform-linters/tflint-plugin-sdk v0.23.0 h1:tvbMuXHIuv3ptT4Z7rtEO7tu6E5GbG+r8QH/JSqijt8=
1292+
github.com/terraform-linters/tflint-plugin-sdk v0.23.0/go.mod h1:D/JrhhuF6Lwf5wC2l3GYQpVAByWq5fVZg5w6XxAiPbs=
12931293
github.com/terraform-linters/tflint-ruleset-terraform v0.13.0 h1:6obXOhxh5e9ijBGhrDm45eMDJiPsDQdYrrdkTI3C2dw=
12941294
github.com/terraform-linters/tflint-ruleset-terraform v0.13.0/go.mod h1:r6nu7KOv86j/oEOcNmwf156BzskCSMWlyKgNV0loaRQ=
12951295
github.com/theupdateframework/go-tuf v0.7.0 h1:CqbQFrWo1ae3/I0UCblSbczevCCbS31Qvs5LdxRWqRI=
@@ -1673,8 +1673,8 @@ golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
16731673
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
16741674
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
16751675
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
1676-
golang.org/x/telemetry v0.0.0-20250807160809-1a19826ec488 h1:3doPGa+Gg4snce233aCWnbZVFsyFMo/dR40KK/6skyE=
1677-
golang.org/x/telemetry v0.0.0-20250807160809-1a19826ec488/go.mod h1:fGb/2+tgXXjhjHsTNdVEEMZNWA0quBnfrO+AfoDSAKw=
1676+
golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053 h1:dHQOQddU4YHS5gY33/6klKjq7Gp3WwMyOXGNp5nzRj8=
1677+
golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053/go.mod h1:+nZKN+XVh4LCiA9DV3ywrzN4gumyCnKjau3NGb9SGoE=
16781678
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
16791679
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
16801680
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
@@ -1783,8 +1783,8 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
17831783
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
17841784
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
17851785
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
1786-
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
1787-
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
1786+
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
1787+
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
17881788
golang.org/x/tools/go/expect v0.1.0-deprecated h1:jY2C5HGYR5lqex3gEniOQL0r7Dq5+VGVgY1nudX5lXY=
17891789
golang.org/x/tools/go/expect v0.1.0-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
17901790
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM=
@@ -2075,8 +2075,8 @@ google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw
20752075
google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
20762076
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
20772077
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
2078-
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
2079-
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
2078+
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
2079+
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
20802080
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
20812081
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
20822082
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=

integrationtest/inspection/inspection_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ func TestIntegration(t *testing.T) {
8080
Command: "./tflint --format json",
8181
Dir: "jsonsyntax",
8282
},
83+
{
84+
Name: "json config with complex plugin settings",
85+
Command: "./tflint --format json",
86+
Dir: "json-config",
87+
},
8388
{
8489
Name: "path",
8590
Command: "./tflint --format json",
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"config": {
3+
"call_module_type": "local",
4+
"force": false,
5+
"disabled_by_default": false,
6+
"ignore_module": {
7+
"./ignore": true
8+
}
9+
},
10+
"plugin": {
11+
"terraform": {
12+
"enabled": true,
13+
"preset": "recommended"
14+
}
15+
},
16+
"rule": {
17+
"terraform_naming_convention": {
18+
"enabled": true
19+
},
20+
"terraform_documented_outputs": {
21+
"enabled": false
22+
},
23+
"terraform_module_pinned_source": {
24+
"enabled": false
25+
},
26+
"terraform_standard_module_structure": {
27+
"enabled": true
28+
}
29+
}
30+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Example tfvars file referenced in .tflint.json
2+
test_var = "value1"
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# This module should be ignored by TFLint
2+
3+
variable "ignored_input" {
4+
type = string
5+
}
6+
7+
output "ignored_output" {
8+
value = var.ignored_input
9+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
terraform {
2+
required_version = ">= 1.0"
3+
}
4+
5+
6+
# This output lacks documentation (but rule is disabled in config)
7+
output "undocumented_output" {
8+
value = "test"
9+
}
10+
11+
# This output has documentation
12+
output "documented_output" {
13+
description = "A properly documented output"
14+
value = var.proper_variable
15+
}
16+
17+
# Module with pinned source - should pass with flexible style
18+
module "pinned_module" {
19+
source = "terraform-aws-modules/vpc/aws"
20+
version = "3.14.0"
21+
}
22+
23+
# Module without pinned source - should fail
24+
module "unpinned_module" {
25+
source = "./modules/local"
26+
}
27+
28+
# Module that should be ignored
29+
module "ignored_module" {
30+
source = "./ignore"
31+
}
32+
33+
# Resource with proper naming
34+
resource "aws_instance" "example_instance" {
35+
ami = "ami-12345678"
36+
instance_type = "t2.micro"
37+
}
38+
39+
# Resource with improper naming (mixed case)
40+
resource "aws_s3_bucket" "TestBucket" {
41+
bucket = "my-test-bucket"
42+
}
43+
44+
# Data source with improper naming
45+
data "aws_ami" "LatestAmi" {
46+
most_recent = true
47+
owners = ["amazon"]
48+
}
49+
50+
# Local value with improper naming
51+
locals {
52+
MixedCaseLocal = "value"
53+
proper_local = "another_value"
54+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Local module for testing
2+
variable "input" {
3+
type = string
4+
default = "test"
5+
}
6+
7+
output "output" {
8+
value = var.input
9+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Standard module structure test file
2+
# This file should satisfy the terraform_standard_module_structure rule
3+
4+
output "vpc_id" {
5+
description = "The ID of the VPC"
6+
value = "vpc-12345678"
7+
}
8+
9+
output "instance_id" {
10+
description = "The ID of the EC2 instance"
11+
value = aws_instance.example_instance.id
12+
}

0 commit comments

Comments
 (0)