Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 12 additions & 26 deletions cmd/inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,12 @@ import (
"os"
"path/filepath"

"github.com/hashicorp/go-version"
"github.com/hashicorp/hcl/v2"
"github.com/spf13/afero"
"github.com/terraform-linters/tflint-plugin-sdk/hclext"
"github.com/terraform-linters/tflint/plugin"
"github.com/terraform-linters/tflint/terraform"
"github.com/terraform-linters/tflint/tflint"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

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

// Check preconditions
sdkVersions := map[string]*version.Version{}
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+.
return issues, changes, fmt.Errorf(`Plugin "%s" SDK version is incompatible. Compatible versions: %s`, name, plugin.SDKVersionConstraints)
} else {
return issues, changes, fmt.Errorf(`Failed to get plugin "%s" SDK version; %w`, name, err)
}
}
if !plugin.SDKVersionConstraints.Check(sdkVersion) {
return issues, changes, fmt.Errorf(`Plugin "%s" SDK version (%s) is incompatible. Compatible versions: %s`, name, sdkVersion, plugin.SDKVersionConstraints)
}
sdkVersions[name] = sdkVersion
// Validate and collect plugin versions
sdkVersions, err := plugin.ValidatePluginVersions(rulesetPlugin, cli.config.IsJSONConfig())
if err != nil {
return issues, changes, err
}

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

// Check version constraints and apply a config to plugins
// Apply config to plugins
for name, ruleset := range rulesetPlugin.RuleSets {
// Check TFLint version constraints before applying config
constraints, err := ruleset.VersionConstraints()
if err != nil {
if st, ok := status.FromError(err); ok && st.Code() == codes.Unimplemented {
if plugin.IsVersionConstraintsUnimplemented(err) {
// VersionConstraints endpoint is available in tflint-plugin-sdk v0.14+.
return rulesetPlugin, fmt.Errorf(`Plugin "%s" SDK version is incompatible. Compatible versions: %s`, name, plugin.SDKVersionConstraints)
} else {
return rulesetPlugin, fmt.Errorf(`Failed to get TFLint version constraints to "%s" plugin; %w`, name, err)
// Plugin is too old
return rulesetPlugin, fmt.Errorf(`Plugin "%s" SDK version is incompatible. Compatible versions: %s`, name, plugin.DefaultSDKVersionConstraints)
}
return rulesetPlugin, fmt.Errorf(`Failed to get TFLint version constraints to "%s" plugin; %w`, name, err)
}
if !constraints.Check(tflint.Version) {
return rulesetPlugin, fmt.Errorf("Failed to satisfy version constraints; tflint-ruleset-%s requires %s, but TFLint version is %s", name, constraints, tflint.Version)
if err := plugin.CheckTFLintVersionConstraints(name, constraints); err != nil {
return rulesetPlugin, err
}

if err := ruleset.ApplyGlobalConfig(pluginConf); err != nil {
Expand Down
45 changes: 42 additions & 3 deletions docs/user-guide/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ You can change the behavior not only in CLI flags but also in config files. TFLi

1. File passed by the `--config` option
2. File set by the `TFLINT_CONFIG_FILE` environment variable
3. Current directory (`./.tflint.hcl`)
4. Home directory (`~/.tflint.hcl`)
3. Current directory `./.tflint.hcl`
4. Current directory `./.tflint.json`
5. Home directory `~/.tflint.hcl`
6. Home directory `~/.tflint.json`

The config file is written in [HCL](https://github.com/hashicorp/hcl). An example is shown below:
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:

```hcl
tflint {
Expand Down Expand Up @@ -42,10 +44,47 @@ rule "aws_instance_invalid_type" {
}
```

The same configuration can be written in JSON format as `.tflint.json`:

```json
{
"tflint": {
"required_version": ">= 0.50"
},
"config": {
"format": "compact",
"plugin_dir": "~/.tflint.d/plugins",
"call_module_type": "local",
"force": false,
"disabled_by_default": false,
"ignore_module": {
"terraform-aws-modules/vpc/aws": true,
"terraform-aws-modules/security-group/aws": true
},
"varfile": ["example1.tfvars", "example2.tfvars"],
"variables": ["foo=bar", "bar=[\"baz\"]"]
},
"plugin": {
"aws": {
"enabled": true,
"version": "0.4.0",
"source": "github.com/terraform-linters/tflint-ruleset-aws"
}
},
"rule": {
"aws_instance_invalid_type": {
"enabled": false
}
}
}
```

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:

```sh
tflint --recursive --config "$(pwd)/.tflint.hcl"
# or
tflint --recursive --config "$(pwd)/.tflint.json"
```

### `required_version`
Expand Down
15 changes: 15 additions & 0 deletions integrationtest/cli/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,21 @@ func TestIntegration(t *testing.T) {
status: cmd.ExitCodeOK,
stdout: "[]",
},
{
name: "JSON format config",
command: "./tflint",
dir: "json_config",
status: cmd.ExitCodeOK,
stdout: `<?xml version="1.0" encoding="UTF-8"?>
<checkstyle></checkstyle>`,
},
{
name: "HCL precedence over JSON config",
command: "./tflint",
dir: "hcl_json_precedence",
status: cmd.ExitCodeOK,
stdout: "",
},
{
name: "`--force` option with no issues",
command: "./tflint --force",
Expand Down
3 changes: 3 additions & 0 deletions integrationtest/cli/hcl_json_precedence/.tflint.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
config {
format = "compact"
}
5 changes: 5 additions & 0 deletions integrationtest/cli/hcl_json_precedence/.tflint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"config": {
"format": "checkstyle"
}
}
5 changes: 5 additions & 0 deletions integrationtest/cli/json_config/.tflint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"config": {
"format": "checkstyle"
}
}
2 changes: 1 addition & 1 deletion integrationtest/inspection/inspection_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package main
package inspection

import (
"bytes"
Expand Down
25 changes: 14 additions & 11 deletions langserver/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ import (
"github.com/terraform-linters/tflint/plugin"
"github.com/terraform-linters/tflint/terraform"
"github.com/terraform-linters/tflint/tflint"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

// NewHandler returns a new JSON-RPC handler
Expand All @@ -48,30 +46,35 @@ func NewHandler(configPath string, cliConfig *tflint.Config) (jsonrpc2.Handler,
for name, ruleset := range rulsetPlugin.RuleSets {
constraints, err := ruleset.VersionConstraints()
if err != nil {
if st, ok := status.FromError(err); ok && st.Code() == codes.Unimplemented {
if plugin.IsVersionConstraintsUnimplemented(err) {
// VersionConstraints endpoint is available in tflint-plugin-sdk v0.14+.
return nil, nil, fmt.Errorf(`Plugin "%s" SDK version is incompatible. Compatible versions: %s`, name, plugin.SDKVersionConstraints)
return nil, nil, fmt.Errorf(`Plugin "%s" SDK version is incompatible. Compatible versions: %s`, name, plugin.DefaultSDKVersionConstraints)
} else {
return nil, nil, fmt.Errorf(`Failed to get TFLint version constraints to "%s" plugin; %w`, name, err)
}
}
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)
if err := plugin.CheckTFLintVersionConstraints(name, constraints); err != nil {
return nil, nil, err
}

clientSDKVersions[name], err = ruleset.SDKVersion()
sdkVersion, err := ruleset.SDKVersion()
if err != nil {
if st, ok := status.FromError(err); ok && st.Code() == codes.Unimplemented {
if plugin.IsSDKVersionUnimplemented(err) {
// SDKVersion endpoint is available in tflint-plugin-sdk v0.14+.
return nil, nil, fmt.Errorf(`Plugin "%s" SDK version is incompatible. Compatible versions: %s`, name, plugin.SDKVersionConstraints)
// Plugin is too old, treat as nil
sdkVersion = nil
} else {
return nil, nil, fmt.Errorf(`Failed to get plugin "%s" SDK version; %w`, name, err)
}
}
if !plugin.SDKVersionConstraints.Check(clientSDKVersions[name]) {
return nil, nil, fmt.Errorf(`Plugin "%s" SDK version (%s) is incompatible. Compatible versions: %s`, name, clientSDKVersions[name], plugin.SDKVersionConstraints)

// Check if plugin SDK version meets minimum requirements for the config type
if err := plugin.CheckSDKVersionSatisfiesConstraints(name, sdkVersion, cfg.IsJSONConfig()); err != nil {
return nil, nil, err
}

clientSDKVersions[name] = sdkVersion

rulesets = append(rulesets, ruleset)
}
if err := cliConfig.ValidateRules(rulesets...); err != nil {
Expand Down
4 changes: 0 additions & 4 deletions plugin/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package plugin

import (
plugin "github.com/hashicorp/go-plugin"
"github.com/hashicorp/go-version"
"github.com/terraform-linters/tflint-plugin-sdk/plugin/host2plugin"
)

Expand All @@ -13,9 +12,6 @@ var (
localPluginRoot = "./.tflint.d/plugins"
)

// SDKVersionConstraints is the version constraint of the supported SDK version.
var SDKVersionConstraints = version.MustConstraints(version.NewConstraint(">= 0.16.0"))

// Plugin is an object handling plugins
// Basically, it is a wrapper for go-plugin and provides an API to handle them collectively.
type Plugin struct {
Expand Down
105 changes: 105 additions & 0 deletions plugin/plugin_version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package plugin

import (
"fmt"

"github.com/hashicorp/go-version"
"github.com/terraform-linters/tflint/tflint"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

// Version constraints for plugin compatibility
var (
// DefaultSDKVersionConstraints is the minimum SDK version for basic functionality
DefaultSDKVersionConstraints = version.MustConstraints(version.NewConstraint(">= 0.16.0"))

// JSONConfigSDKVersionConstraints is the minimum SDK version for JSON configuration support
JSONConfigSDKVersionConstraints = version.MustConstraints(version.NewConstraint(">= 0.23.0"))

// EphemeralMarksMinVersion is when ephemeral marks support was added
EphemeralMarksMinVersion = version.Must(version.NewVersion("0.22.0"))
)

// CheckTFLintVersionConstraints validates if TFLint version meets the plugin's requirements
func CheckTFLintVersionConstraints(pluginName string, constraints version.Constraints) error {
if !constraints.Check(tflint.Version) {
return fmt.Errorf("Failed to satisfy version constraints; tflint-ruleset-%s requires %s, but TFLint version is %s", pluginName, constraints, tflint.Version)
}
return nil
}

// CheckSDKVersionSatisfiesConstraints validates if a plugin's SDK version meets the minimum requirements.
// For HCL configs, requires SDK >= 0.16.0. For JSON configs, requires SDK >= 0.23.0.
func CheckSDKVersionSatisfiesConstraints(pluginName string, sdkVersion *version.Version, isJSONConfig bool) error {
// If sdkVersion is nil, the plugin doesn't have SDKVersion endpoint (SDK < 0.14)
if sdkVersion == nil {
return fmt.Errorf(`Plugin "%s" SDK version is incompatible. Compatible versions: %s`, pluginName, DefaultSDKVersionConstraints)
}

constraints := DefaultSDKVersionConstraints
if isJSONConfig {
constraints = JSONConfigSDKVersionConstraints
}

if !constraints.Check(sdkVersion) {
if isJSONConfig {
return fmt.Errorf(`Plugin "%s" SDK version (%s) is incompatible with JSON configuration. Minimum required: %s`, pluginName, sdkVersion, JSONConfigSDKVersionConstraints)
}
return fmt.Errorf(`Plugin "%s" SDK version (%s) is incompatible. Compatible versions: %s`, pluginName, sdkVersion, DefaultSDKVersionConstraints)
}
return nil
}

// SupportsEphemeralMarks checks if the plugin SDK version supports ephemeral marks
func SupportsEphemeralMarks(sdkVersion *version.Version) bool {
if sdkVersion == nil {
return false
}
return sdkVersion.GreaterThanOrEqual(EphemeralMarksMinVersion)
}

// IsSDKVersionUnimplemented checks if an error indicates the SDK version endpoint is not implemented
func IsSDKVersionUnimplemented(err error) bool {
if st, ok := status.FromError(err); ok {
return st.Code() == codes.Unimplemented
}
return false
}

// IsVersionConstraintsUnimplemented checks if an error indicates the version constraints endpoint is not implemented
func IsVersionConstraintsUnimplemented(err error) bool {
if st, ok := status.FromError(err); ok {
return st.Code() == codes.Unimplemented
}
return false
}

// ValidatePluginVersions checks plugin SDK version requirements and returns SDK versions for later use
// Note: TFLint version constraints are checked separately in launchPlugins before ApplyGlobalConfig
func ValidatePluginVersions(rulesetPlugin *Plugin, isJSONConfig bool) (map[string]*version.Version, error) {
sdkVersions := map[string]*version.Version{}

for name, ruleset := range rulesetPlugin.RuleSets {
// Get SDK version
sdkVersion, err := ruleset.SDKVersion()
if err != nil {
if IsSDKVersionUnimplemented(err) {
// SDKVersion endpoint is available in tflint-plugin-sdk v0.14+.
// Plugin is too old, treat as nil
sdkVersion = nil
} else {
return nil, fmt.Errorf(`Failed to get plugin "%s" SDK version; %w`, name, err)
}
}

// Check if SDK version meets minimum requirements
if err := CheckSDKVersionSatisfiesConstraints(name, sdkVersion, isJSONConfig); err != nil {
return nil, err
}

sdkVersions[name] = sdkVersion
}

return sdkVersions, nil
}
Loading
Loading