Skip to content

Commit 0807dfb

Browse files
feat: add JSON configuration file support with centralized plugin version checking
- Add support for JSON configuration files (.tflint.json) - Detect JSON configs by file extension, not filename pattern - Centralize all plugin version checking logic into plugin/plugin_version.go - Enforce SDK version 0.23+ for JSON configuration support - Split version checking: TFLint constraints checked before ApplyGlobalConfig, SDK versions checked after to avoid plugin initialization failures - Add comprehensive unit tests for SDK version validation - Update documentation to mention JSON configuration support This allows users to use JSON configuration files with any name when using the --config flag, while ensuring proper plugin compatibility.
1 parent dd89781 commit 0807dfb

File tree

14 files changed

+759
-54
lines changed

14 files changed

+759
-54
lines changed

cmd/inspect.go

Lines changed: 12 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,12 @@ import (
88
"os"
99
"path/filepath"
1010

11-
"github.com/hashicorp/go-version"
1211
"github.com/hashicorp/hcl/v2"
1312
"github.com/spf13/afero"
1413
"github.com/terraform-linters/tflint-plugin-sdk/hclext"
1514
"github.com/terraform-linters/tflint/plugin"
1615
"github.com/terraform-linters/tflint/terraform"
1716
"github.com/terraform-linters/tflint/tflint"
18-
"google.golang.org/grpc/codes"
19-
"google.golang.org/grpc/status"
2017
)
2118

2219
func (cli *CLI) inspect(opts Options) int {
@@ -125,22 +122,10 @@ func (cli *CLI) inspectModule(opts Options, dir string, filterFiles []string) (t
125122
return issues, changes, err
126123
}
127124

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
125+
// Validate and collect plugin versions
126+
sdkVersions, err := plugin.ValidatePluginVersions(rulesetPlugin, cli.config.IsJSONConfig())
127+
if err != nil {
128+
return issues, changes, err
144129
}
145130

146131
// Run inspection
@@ -265,19 +250,20 @@ func launchPlugins(config *tflint.Config, fix bool) (*plugin.Plugin, error) {
265250
pluginConf := config.ToPluginConfig()
266251
pluginConf.Fix = fix
267252

268-
// Check version constraints and apply a config to plugins
253+
// Apply config to plugins
269254
for name, ruleset := range rulesetPlugin.RuleSets {
255+
// Check TFLint version constraints before applying config
270256
constraints, err := ruleset.VersionConstraints()
271257
if err != nil {
272-
if st, ok := status.FromError(err); ok && st.Code() == codes.Unimplemented {
258+
if plugin.IsVersionConstraintsUnimplemented(err) {
273259
// 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)
275-
} else {
276-
return rulesetPlugin, fmt.Errorf(`Failed to get TFLint version constraints to "%s" plugin; %w`, name, err)
260+
// Plugin is too old
261+
return rulesetPlugin, fmt.Errorf(`Plugin "%s" SDK version is incompatible. Compatible versions: %s`, name, plugin.DefaultSDKVersionConstraints)
277262
}
263+
return rulesetPlugin, fmt.Errorf(`Failed to get TFLint version constraints to "%s" plugin; %w`, name, err)
278264
}
279-
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)
265+
if err := plugin.CheckTFLintVersionConstraints(name, constraints); err != nil {
266+
return rulesetPlugin, err
281267
}
282268

283269
if err := ruleset.ApplyGlobalConfig(pluginConf); err != nil {

docs/user-guide/config.md

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ You can change the behavior not only in CLI flags but also in config files. TFLi
44

55
1. File passed by the `--config` option
66
2. File set by the `TFLINT_CONFIG_FILE` environment variable
7-
3. Current directory (`./.tflint.hcl`)
8-
4. Home directory (`~/.tflint.hcl`)
7+
3. Current directory `./.tflint.hcl`
8+
4. Current directory `./.tflint.json`
9+
5. Home directory `~/.tflint.hcl`
10+
6. Home directory `~/.tflint.json`
911

10-
The config file is written in [HCL](https://github.com/hashicorp/hcl). An example is shown below:
12+
The config file can be written in either [HCL](https://github.com/hashicorp/hcl) or JSON format, determined by the file extension. JSON files use the [HCL-compatible JSON syntax](https://developer.hashicorp.com/terraform/language/syntax/json), following the same structure as Terraform's `.tf.json` files. An HCL example is shown below:
1113

1214
```hcl
1315
tflint {
@@ -42,10 +44,47 @@ rule "aws_instance_invalid_type" {
4244
}
4345
```
4446

47+
The same configuration can be written in JSON format as `.tflint.json`:
48+
49+
```json
50+
{
51+
"tflint": {
52+
"required_version": ">= 0.50"
53+
},
54+
"config": {
55+
"format": "compact",
56+
"plugin_dir": "~/.tflint.d/plugins",
57+
"call_module_type": "local",
58+
"force": false,
59+
"disabled_by_default": false,
60+
"ignore_module": {
61+
"terraform-aws-modules/vpc/aws": true,
62+
"terraform-aws-modules/security-group/aws": true
63+
},
64+
"varfile": ["example1.tfvars", "example2.tfvars"],
65+
"variables": ["foo=bar", "bar=[\"baz\"]"]
66+
},
67+
"plugin": {
68+
"aws": {
69+
"enabled": true,
70+
"version": "0.4.0",
71+
"source": "github.com/terraform-linters/tflint-ruleset-aws"
72+
}
73+
},
74+
"rule": {
75+
"aws_instance_invalid_type": {
76+
"enabled": false
77+
}
78+
}
79+
}
80+
```
81+
4582
The file path is resolved relative to the module directory when `--chdir` or `--recursive` is used. To use a config file from the working directory when recursing, pass an absolute path:
4683

4784
```sh
4885
tflint --recursive --config "$(pwd)/.tflint.hcl"
86+
# or
87+
tflint --recursive --config "$(pwd)/.tflint.json"
4988
```
5089

5190
### `required_version`

integrationtest/cli/cli_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,21 @@ func TestIntegration(t *testing.T) {
7171
status: cmd.ExitCodeOK,
7272
stdout: "[]",
7373
},
74+
{
75+
name: "JSON format config",
76+
command: "./tflint",
77+
dir: "json_config",
78+
status: cmd.ExitCodeOK,
79+
stdout: `<?xml version="1.0" encoding="UTF-8"?>
80+
<checkstyle></checkstyle>`,
81+
},
82+
{
83+
name: "HCL precedence over JSON config",
84+
command: "./tflint",
85+
dir: "hcl_json_precedence",
86+
status: cmd.ExitCodeOK,
87+
stdout: "",
88+
},
7489
{
7590
name: "`--force` option with no issues",
7691
command: "./tflint --force",
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
config {
2+
format = "compact"
3+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"config": {
3+
"format": "checkstyle"
4+
}
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"config": {
3+
"format": "checkstyle"
4+
}
5+
}

integrationtest/inspection/inspection_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package main
1+
package inspection
22

33
import (
44
"bytes"

langserver/handler.go

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@ import (
2121
"github.com/terraform-linters/tflint/plugin"
2222
"github.com/terraform-linters/tflint/terraform"
2323
"github.com/terraform-linters/tflint/tflint"
24-
"google.golang.org/grpc/codes"
25-
"google.golang.org/grpc/status"
2624
)
2725

2826
// NewHandler returns a new JSON-RPC handler
@@ -48,30 +46,35 @@ func NewHandler(configPath string, cliConfig *tflint.Config) (jsonrpc2.Handler,
4846
for name, ruleset := range rulsetPlugin.RuleSets {
4947
constraints, err := ruleset.VersionConstraints()
5048
if err != nil {
51-
if st, ok := status.FromError(err); ok && st.Code() == codes.Unimplemented {
49+
if plugin.IsVersionConstraintsUnimplemented(err) {
5250
// VersionConstraints endpoint is available in tflint-plugin-sdk v0.14+.
53-
return nil, nil, fmt.Errorf(`Plugin "%s" SDK version is incompatible. Compatible versions: %s`, name, plugin.SDKVersionConstraints)
51+
return nil, nil, fmt.Errorf(`Plugin "%s" SDK version is incompatible. Compatible versions: %s`, name, plugin.DefaultSDKVersionConstraints)
5452
} else {
5553
return nil, nil, fmt.Errorf(`Failed to get TFLint version constraints to "%s" plugin; %w`, name, err)
5654
}
5755
}
58-
if !constraints.Check(tflint.Version) {
59-
return nil, nil, fmt.Errorf("Failed to satisfy version constraints; tflint-ruleset-%s requires %s, but TFLint version is %s", name, constraints, tflint.Version)
56+
if err := plugin.CheckTFLintVersionConstraints(name, constraints); err != nil {
57+
return nil, nil, err
6058
}
6159

62-
clientSDKVersions[name], err = ruleset.SDKVersion()
60+
sdkVersion, err := ruleset.SDKVersion()
6361
if err != nil {
64-
if st, ok := status.FromError(err); ok && st.Code() == codes.Unimplemented {
62+
if plugin.IsSDKVersionUnimplemented(err) {
6563
// SDKVersion endpoint is available in tflint-plugin-sdk v0.14+.
66-
return nil, nil, fmt.Errorf(`Plugin "%s" SDK version is incompatible. Compatible versions: %s`, name, plugin.SDKVersionConstraints)
64+
// Plugin is too old, treat as nil
65+
sdkVersion = nil
6766
} else {
6867
return nil, nil, fmt.Errorf(`Failed to get plugin "%s" SDK version; %w`, name, err)
6968
}
7069
}
71-
if !plugin.SDKVersionConstraints.Check(clientSDKVersions[name]) {
72-
return nil, nil, fmt.Errorf(`Plugin "%s" SDK version (%s) is incompatible. Compatible versions: %s`, name, clientSDKVersions[name], plugin.SDKVersionConstraints)
70+
71+
// Check if plugin SDK version meets minimum requirements for the config type
72+
if err := plugin.CheckSDKVersionSatisfiesConstraints(name, sdkVersion, cfg.IsJSONConfig()); err != nil {
73+
return nil, nil, err
7374
}
7475

76+
clientSDKVersions[name] = sdkVersion
77+
7578
rulesets = append(rulesets, ruleset)
7679
}
7780
if err := cliConfig.ValidateRules(rulesets...); err != nil {

plugin/plugin.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package plugin
22

33
import (
44
plugin "github.com/hashicorp/go-plugin"
5-
"github.com/hashicorp/go-version"
65
"github.com/terraform-linters/tflint-plugin-sdk/plugin/host2plugin"
76
)
87

@@ -13,9 +12,6 @@ var (
1312
localPluginRoot = "./.tflint.d/plugins"
1413
)
1514

16-
// SDKVersionConstraints is the version constraint of the supported SDK version.
17-
var SDKVersionConstraints = version.MustConstraints(version.NewConstraint(">= 0.16.0"))
18-
1915
// Plugin is an object handling plugins
2016
// Basically, it is a wrapper for go-plugin and provides an API to handle them collectively.
2117
type Plugin struct {

plugin/plugin_version.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package plugin
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/hashicorp/go-version"
7+
"github.com/terraform-linters/tflint/tflint"
8+
"google.golang.org/grpc/codes"
9+
"google.golang.org/grpc/status"
10+
)
11+
12+
// Version constraints for plugin compatibility
13+
var (
14+
// DefaultSDKVersionConstraints is the minimum SDK version for basic functionality
15+
DefaultSDKVersionConstraints = version.MustConstraints(version.NewConstraint(">= 0.16.0"))
16+
17+
// JSONConfigSDKVersionConstraints is the minimum SDK version for JSON configuration support
18+
JSONConfigSDKVersionConstraints = version.MustConstraints(version.NewConstraint(">= 0.23.0"))
19+
20+
// EphemeralMarksMinVersion is when ephemeral marks support was added
21+
EphemeralMarksMinVersion = version.Must(version.NewVersion("0.22.0"))
22+
)
23+
24+
// CheckTFLintVersionConstraints validates if TFLint version meets the plugin's requirements
25+
func CheckTFLintVersionConstraints(pluginName string, constraints version.Constraints) error {
26+
if !constraints.Check(tflint.Version) {
27+
return fmt.Errorf("Failed to satisfy version constraints; tflint-ruleset-%s requires %s, but TFLint version is %s", pluginName, constraints, tflint.Version)
28+
}
29+
return nil
30+
}
31+
32+
// CheckSDKVersionSatisfiesConstraints validates if a plugin's SDK version meets the minimum requirements.
33+
// For HCL configs, requires SDK >= 0.16.0. For JSON configs, requires SDK >= 0.23.0.
34+
func CheckSDKVersionSatisfiesConstraints(pluginName string, sdkVersion *version.Version, isJSONConfig bool) error {
35+
// If sdkVersion is nil, the plugin doesn't have SDKVersion endpoint (SDK < 0.14)
36+
if sdkVersion == nil {
37+
return fmt.Errorf(`Plugin "%s" SDK version is incompatible. Compatible versions: %s`, pluginName, DefaultSDKVersionConstraints)
38+
}
39+
40+
constraints := DefaultSDKVersionConstraints
41+
if isJSONConfig {
42+
constraints = JSONConfigSDKVersionConstraints
43+
}
44+
45+
if !constraints.Check(sdkVersion) {
46+
if isJSONConfig {
47+
return fmt.Errorf(`Plugin "%s" SDK version (%s) is incompatible with JSON configuration. Minimum required: %s`, pluginName, sdkVersion, JSONConfigSDKVersionConstraints)
48+
}
49+
return fmt.Errorf(`Plugin "%s" SDK version (%s) is incompatible. Compatible versions: %s`, pluginName, sdkVersion, DefaultSDKVersionConstraints)
50+
}
51+
return nil
52+
}
53+
54+
// SupportsEphemeralMarks checks if the plugin SDK version supports ephemeral marks
55+
func SupportsEphemeralMarks(sdkVersion *version.Version) bool {
56+
if sdkVersion == nil {
57+
return false
58+
}
59+
return sdkVersion.GreaterThanOrEqual(EphemeralMarksMinVersion)
60+
}
61+
62+
// IsSDKVersionUnimplemented checks if an error indicates the SDK version endpoint is not implemented
63+
func IsSDKVersionUnimplemented(err error) bool {
64+
if st, ok := status.FromError(err); ok {
65+
return st.Code() == codes.Unimplemented
66+
}
67+
return false
68+
}
69+
70+
// IsVersionConstraintsUnimplemented checks if an error indicates the version constraints endpoint is not implemented
71+
func IsVersionConstraintsUnimplemented(err error) bool {
72+
if st, ok := status.FromError(err); ok {
73+
return st.Code() == codes.Unimplemented
74+
}
75+
return false
76+
}
77+
78+
// ValidatePluginVersions checks plugin SDK version requirements and returns SDK versions for later use
79+
// Note: TFLint version constraints are checked separately in launchPlugins before ApplyGlobalConfig
80+
func ValidatePluginVersions(rulesetPlugin *Plugin, isJSONConfig bool) (map[string]*version.Version, error) {
81+
sdkVersions := map[string]*version.Version{}
82+
83+
for name, ruleset := range rulesetPlugin.RuleSets {
84+
// Get SDK version
85+
sdkVersion, err := ruleset.SDKVersion()
86+
if err != nil {
87+
if IsSDKVersionUnimplemented(err) {
88+
// SDKVersion endpoint is available in tflint-plugin-sdk v0.14+.
89+
// Plugin is too old, treat as nil
90+
sdkVersion = nil
91+
} else {
92+
return nil, fmt.Errorf(`Failed to get plugin "%s" SDK version; %w`, name, err)
93+
}
94+
}
95+
96+
// Check if SDK version meets minimum requirements
97+
if err := CheckSDKVersionSatisfiesConstraints(name, sdkVersion, isJSONConfig); err != nil {
98+
return nil, err
99+
}
100+
101+
sdkVersions[name] = sdkVersion
102+
}
103+
104+
return sdkVersions, nil
105+
}

0 commit comments

Comments
 (0)