Skip to content

Commit c84a32a

Browse files
feat: Add JSON config validation and SDK compatibility shim
This commit enhances JSON configuration support with filename validation, SDK compatibility handling, and comprehensive integration tests. ## JSON Config Filename Validation JSON configuration files must now end with "tflint.json" to avoid confusion with other JSON files like terraform.tfvars.json. Valid names: - .tflint.json - my-tflint.json - project.tflint.json Invalid names: - config.json - test.json - terraform.tfvars.json ## SDK Compatibility Shim Added a compatibility shim for plugins using SDK version < 0.23.0 that cannot parse JSON config files with the .json extension. The shim: - Detects plugin SDK versions during initialization - Re-parses JSON configs with .tf.json extension for older SDKs - Warns users about compatibility issues - Ensures all plugin rules work correctly ## Implementation Details - Added validation in config.go to check JSON filename suffixes - Implemented ReparseForCompatibility() method for config re-parsing - Modified launchPlugins() to detect SDK versions and apply shim - Updated all config tests to use valid JSON filenames - Added comprehensive integration test for JSON config with plugins - Applied gofmt formatting across affected files The compatibility shim is applied transparently when needed, with appropriate warnings logged to inform users about the compatibility mode being used. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 29f637e commit c84a32a

File tree

16 files changed

+389
-59
lines changed

16 files changed

+389
-59
lines changed

cmd/inspect.go

Lines changed: 42 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,11 @@ 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+
// Note: launchPlugins may return an updated config if JSON files need renaming for SDK compatibility
117+
rulesetPlugin, updatedConfig, sdkVersions, err := launchPlugins(cli.config, opts.Fix)
118+
if updatedConfig != nil {
119+
cli.config = updatedConfig // Update config with re-parsed version (JSON files may be renamed to .tf.json)
120+
}
117121
if rulesetPlugin != nil {
118122
defer rulesetPlugin.Clean()
119123
go cli.registerShutdownHandler(func() {
@@ -125,24 +129,6 @@ func (cli *CLI) inspectModule(opts Options, dir string, filterFiles []string) (t
125129
return issues, changes, err
126130
}
127131

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)
138-
}
139-
}
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
144-
}
145-
146132
// Run inspection
147133
//
148134
// Repeat an inspection until there are no more changes or the limit is reached,
@@ -254,11 +240,37 @@ func (cli *CLI) setupRunners(opts Options, dir string) (*tflint.Runner, []*tflin
254240
return runner, moduleRunners, nil
255241
}
256242

257-
func launchPlugins(config *tflint.Config, fix bool) (*plugin.Plugin, error) {
243+
func launchPlugins(config *tflint.Config, fix bool) (*plugin.Plugin, *tflint.Config, map[string]*version.Version, error) {
258244
// Lookup plugins
259245
rulesetPlugin, err := plugin.Discovery(config)
260246
if err != nil {
261-
return nil, fmt.Errorf("Failed to initialize plugins; %w", err)
247+
return nil, nil, nil, fmt.Errorf("Failed to initialize plugins; %w", err)
248+
}
249+
250+
// First, collect SDK versions from all plugins
251+
sdkVersions := map[string]*version.Version{}
252+
for name, ruleset := range rulesetPlugin.RuleSets {
253+
sdkVersion, err := ruleset.SDKVersion()
254+
if err != nil {
255+
if st, ok := status.FromError(err); ok && st.Code() == codes.Unimplemented {
256+
// SDKVersion endpoint is available in tflint-plugin-sdk v0.14+.
257+
// Assume old version for compatibility checking
258+
sdkVersions[name] = version.Must(version.NewVersion("0.13.0"))
259+
} else {
260+
return nil, nil, nil, fmt.Errorf(`Failed to get plugin "%s" SDK version; %w`, name, err)
261+
}
262+
}
263+
// Check SDK version compatibility
264+
if !plugin.SDKVersionConstraints.Check(sdkVersion) {
265+
return nil, nil, nil, fmt.Errorf(`Plugin "%s" SDK version (%s) is incompatible. Compatible versions: %s`, name, sdkVersion, plugin.SDKVersionConstraints)
266+
}
267+
sdkVersions[name] = sdkVersion
268+
}
269+
270+
// Apply compatibility shim if needed by re-parsing config
271+
config, err = config.ReparseForCompatibility(sdkVersions)
272+
if err != nil {
273+
return nil, nil, nil, fmt.Errorf("Failed to apply compatibility shim: %w", err)
262274
}
263275

264276
rulesets := []tflint.RuleSet{}
@@ -271,44 +283,44 @@ func launchPlugins(config *tflint.Config, fix bool) (*plugin.Plugin, error) {
271283
if err != nil {
272284
if st, ok := status.FromError(err); ok && st.Code() == codes.Unimplemented {
273285
// 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)
286+
return rulesetPlugin, config, sdkVersions, fmt.Errorf(`Plugin "%s" SDK version is incompatible. Compatible versions: %s`, name, plugin.SDKVersionConstraints)
275287
} else {
276-
return rulesetPlugin, fmt.Errorf(`Failed to get TFLint version constraints to "%s" plugin; %w`, name, err)
288+
return rulesetPlugin, config, sdkVersions, fmt.Errorf(`Failed to get TFLint version constraints to "%s" plugin; %w`, name, err)
277289
}
278290
}
279291
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)
292+
return rulesetPlugin, config, sdkVersions, fmt.Errorf("Failed to satisfy version constraints; tflint-ruleset-%s requires %s, but TFLint version is %s", name, constraints, tflint.Version)
281293
}
282294

283295
if err := ruleset.ApplyGlobalConfig(pluginConf); err != nil {
284-
return rulesetPlugin, fmt.Errorf(`Failed to apply global config to "%s" plugin; %w`, name, err)
296+
return rulesetPlugin, config, sdkVersions, fmt.Errorf(`Failed to apply global config to "%s" plugin; %w`, name, err)
285297
}
286298
configSchema, err := ruleset.ConfigSchema()
287299
if err != nil {
288-
return rulesetPlugin, fmt.Errorf(`Failed to fetch config schema from "%s" plugin; %w`, name, err)
300+
return rulesetPlugin, config, sdkVersions, fmt.Errorf(`Failed to fetch config schema from "%s" plugin; %w`, name, err)
289301
}
290302
content := &hclext.BodyContent{}
291303
if plugin, exists := config.Plugins[name]; exists {
292304
var diags hcl.Diagnostics
293305
content, diags = plugin.Content(configSchema)
294306
if diags.HasErrors() {
295-
return rulesetPlugin, fmt.Errorf(`Failed to parse "%s" plugin config; %w`, name, diags)
307+
return rulesetPlugin, config, sdkVersions, fmt.Errorf(`Failed to parse "%s" plugin config; %w`, name, diags)
296308
}
297309
}
298310
err = ruleset.ApplyConfig(content, config.Sources())
299311
if err != nil {
300-
return rulesetPlugin, fmt.Errorf(`Failed to apply config to "%s" plugin; %w`, name, err)
312+
return rulesetPlugin, config, sdkVersions, fmt.Errorf(`Failed to apply config to "%s" plugin; %w`, name, err)
301313
}
302314

303315
rulesets = append(rulesets, ruleset)
304316
}
305317

306318
// Validate config for plugins
307319
if err := config.ValidateRules(rulesets...); err != nil {
308-
return rulesetPlugin, fmt.Errorf("Failed to check rule config; %w", err)
320+
return rulesetPlugin, config, sdkVersions, fmt.Errorf("Failed to check rule config; %w", err)
309321
}
310322

311-
return rulesetPlugin, nil
323+
return rulesetPlugin, config, sdkVersions, nil
312324
}
313325

314326
func writeChanges(changes map[string][]byte) error {

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",

integrationtest/inspection/json-config/.tflint.d/plugins/.gitkeep

Whitespace-only changes.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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": true,
25+
"style": "flexible"
26+
},
27+
"terraform_standard_module_structure": {
28+
"enabled": true
29+
}
30+
}
31+
}
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: 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 = "value2"
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: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
terraform {
2+
required_version = ">= 1.0"
3+
}
4+
5+
# This variable name should trigger terraform_naming_convention rule
6+
variable "TestVariable" {
7+
type = string
8+
description = "A test variable with improper naming"
9+
}
10+
11+
# This output lacks documentation (but rule is disabled in config)
12+
output "undocumented_output" {
13+
value = "test"
14+
}
15+
16+
# This output has documentation
17+
output "documented_output" {
18+
description = "A properly documented output"
19+
value = var.TestVariable
20+
}
21+
22+
# Module with pinned source - should pass with flexible style
23+
module "pinned_module" {
24+
source = "terraform-aws-modules/vpc/aws"
25+
version = "3.14.0"
26+
}
27+
28+
# Module without pinned source - should fail
29+
module "unpinned_module" {
30+
source = "./modules/local"
31+
}
32+
33+
# Module that should be ignored
34+
module "ignored_module" {
35+
source = "./ignore"
36+
}
37+
38+
# Resource with proper naming
39+
resource "aws_instance" "example_instance" {
40+
ami = "ami-12345678"
41+
instance_type = "t2.micro"
42+
}
43+
44+
# Resource with improper naming (mixed case)
45+
resource "aws_s3_bucket" "TestBucket" {
46+
bucket = "my-test-bucket"
47+
}
48+
49+
# Data source with improper naming
50+
data "aws_ami" "LatestAmi" {
51+
most_recent = true
52+
owners = ["amazon"]
53+
}
54+
55+
# Local value with improper naming
56+
locals {
57+
MixedCaseLocal = "value"
58+
proper_local = "another_value"
59+
}
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)