From 9ace8cd4ebf0bef82d25c5bec2a1b116619d9b54 Mon Sep 17 00:00:00 2001 From: Andrew Peabody Date: Fri, 2 Aug 2024 00:31:33 +0000 Subject: [PATCH] feat(tflint-ruleset): add terraform_required_version --- go.work | 12 +- tflint-ruleset-blueprint/go.mod | 10 +- .../rules/terraform_required_version.go | 138 +++++++++++++++++ .../rules/terraform_required_version_test.go | 29 ++++ .../multiple-invalid/.expected/issues.json | 142 ++++++++++++++++++ .../multiple-invalid/main.tf | 39 +++++ .../multiple-valid/.expected/issues.json | 1 + .../multiple-valid/main.tf | 39 +++++ 8 files changed, 400 insertions(+), 10 deletions(-) create mode 100644 tflint-ruleset-blueprint/rules/terraform_required_version.go create mode 100644 tflint-ruleset-blueprint/rules/terraform_required_version_test.go create mode 100644 tflint-ruleset-blueprint/rules/testdata/terraform_required_version/multiple-invalid/.expected/issues.json create mode 100644 tflint-ruleset-blueprint/rules/testdata/terraform_required_version/multiple-invalid/main.tf create mode 100644 tflint-ruleset-blueprint/rules/testdata/terraform_required_version/multiple-valid/.expected/issues.json create mode 100644 tflint-ruleset-blueprint/rules/testdata/terraform_required_version/multiple-valid/main.tf diff --git a/go.work b/go.work index e6835f5f8de3..a2e06c98acca 100644 --- a/go.work +++ b/go.work @@ -1,9 +1,9 @@ -go 1.22 +go 1.22.2 use ( - ./cli - ./infra/blueprint-test - ./infra/module-swapper - ./infra/utils/fbf - ./tflint-ruleset-blueprint + ./cli + ./infra/blueprint-test + ./infra/module-swapper + ./infra/utils/fbf + ./tflint-ruleset-blueprint ) diff --git a/tflint-ruleset-blueprint/go.mod b/tflint-ruleset-blueprint/go.mod index 7bb4d53da007..275497b92820 100644 --- a/tflint-ruleset-blueprint/go.mod +++ b/tflint-ruleset-blueprint/go.mod @@ -1,10 +1,14 @@ module github.com/cloud-foundation-toolkit/tflint-ruleset-blueprint -go 1.22 +go 1.22.2 toolchain go1.22.5 -require github.com/terraform-linters/tflint-plugin-sdk v0.21.0 +require ( + github.com/hashicorp/go-version v1.7.0 + github.com/hashicorp/hcl/v2 v2.21.0 + github.com/terraform-linters/tflint-plugin-sdk v0.21.0 +) require ( github.com/agext/levenshtein v1.2.1 // indirect @@ -14,8 +18,6 @@ require ( github.com/google/go-cmp v0.6.0 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-plugin v1.6.1 // indirect - github.com/hashicorp/go-version v1.7.0 // indirect - github.com/hashicorp/hcl/v2 v2.21.0 // indirect github.com/hashicorp/yamux v0.1.1 // indirect github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.14 // indirect diff --git a/tflint-ruleset-blueprint/rules/terraform_required_version.go b/tflint-ruleset-blueprint/rules/terraform_required_version.go new file mode 100644 index 000000000000..eebf1f6101ef --- /dev/null +++ b/tflint-ruleset-blueprint/rules/terraform_required_version.go @@ -0,0 +1,138 @@ +package rules + +import ( + "fmt" + "strconv" + "strings" + + "github.com/hashicorp/go-version" + "github.com/hashicorp/hcl/v2/gohcl" + "github.com/terraform-linters/tflint-plugin-sdk/hclext" + "github.com/terraform-linters/tflint-plugin-sdk/tflint" +) + +// TerraformRequiredVersion checks if a module has a terraform required_version within valid range. +type TerraformRequiredVersion struct { + tflint.DefaultRule +} + +// NewTerraformRequiredVersion returns a new rule. +func NewTerraformRequiredVersion() *TerraformRequiredVersion { + return &TerraformRequiredVersion{} +} + +// Name returns the rule name. +func (r *TerraformRequiredVersion) Name() string { + return "terraform_required_version" +} + +// Enabled returns whether the rule is enabled by default. +func (r *TerraformRequiredVersion) Enabled() bool { + return false +} + +// Severity returns the rule severity. +func (r *TerraformRequiredVersion) Severity() tflint.Severity { + return tflint.ERROR +} + +// Link returns the rule reference link +func (r *TerraformRequiredVersion) Link() string { + return "https://googlecloudplatform.github.io/samples-style-guide/#language-specific" +} + +const ( + minimum_terraform_required_version = "1.3" + maximum_terraform_required_version = "1.5" +) + +// Check checks whether config contains restricted resource types. +func (r *TerraformRequiredVersion) Check(runner tflint.Runner) error { + splitVersion := strings.Split(minimum_terraform_required_version, ".") + majorVersion, err := strconv.Atoi(splitVersion[0]) + if err != nil { + return err + } + minorVersion, err := strconv.Atoi(splitVersion[1]) + if err != nil { + return err + } + + var terraform_below_minimum_required_version string + if minorVersion > 0 { + terraform_below_minimum_required_version = fmt.Sprintf( + "v%d.%d.999", + majorVersion, + minorVersion - 1, + ) + } else { + terraform_below_minimum_required_version = fmt.Sprintf( + "v%d.%d.999", + majorVersion - 1, + 999, + ) + } + + below_required_version, err := version.NewVersion(terraform_below_minimum_required_version) + if err != nil { + return err + } + + minimum_required_version, err := version.NewVersion(minimum_terraform_required_version) + if err != nil { + return err + } + + maximum_required_version, err := version.NewVersion(maximum_terraform_required_version) + if err != nil { + return err + } + + path, err := runner.GetModulePath() + if err != nil { + return err + } + + if !path.IsRoot() { + return nil + } + + content, err := runner.GetModuleContent(&hclext.BodySchema{ + Blocks: []hclext.BlockSchema{ + { + Type: "terraform", + Body: &hclext.BodySchema{ + Attributes: []hclext.AttributeSchema{{Name: "required_version"}}, + }, + }, + }, + }, &tflint.GetModuleContentOption{ExpandMode: tflint.ExpandModeNone}) + if err != nil { + return err + } + + for _, block := range content.Blocks { + var raw_terraform_required_version string + diags := gohcl.DecodeExpression(block.Body.Attributes["required_version"].Expr, nil, &raw_terraform_required_version) + if diags.HasErrors() { + return fmt.Errorf("failed to decode terraform required_version %q: %v", block.Labels[0], diags.Error()) + } + + constraints, err := version.NewConstraint(raw_terraform_required_version) + if err != nil { + return err + } + + //TODO: add option for exemptions + if !((constraints.Check(minimum_required_version) || constraints.Check(maximum_required_version)) && !constraints.Check(below_required_version)) { + //TODO: use EmitIssueWithFix() + err := runner.EmitIssue(r, fmt.Sprintf("required_version is not inclusive of the the minimum %q and maximum %q terraform required_version: %q", minimum_terraform_required_version, maximum_terraform_required_version, constraints.String()), block.DefRange) + if err != nil { + return err + } + } + + } + + return nil +} diff --git a/tflint-ruleset-blueprint/rules/terraform_required_version_test.go b/tflint-ruleset-blueprint/rules/terraform_required_version_test.go new file mode 100644 index 000000000000..d1e0f832042e --- /dev/null +++ b/tflint-ruleset-blueprint/rules/terraform_required_version_test.go @@ -0,0 +1,29 @@ +package rules + +import ( + "path" + "testing" +) + +const ( + terraformRequiredVersionTestDir = "terraform_required_version" +) + +func TestTerraformMinimumRequiredVersion(t *testing.T) { + tests := []ruleTC{ + { + dir: path.Join(terraformRequiredVersionTestDir, "multiple-valid"), + }, + { + dir: path.Join(terraformRequiredVersionTestDir, "multiple-invalid"), + }, + } + + rule := NewTerraformRequiredVersion() + + for _, tc := range tests { + t.Run(tc.dir, func(t *testing.T) { + ruleTest(t, rule, tc) + }) + } +} diff --git a/tflint-ruleset-blueprint/rules/testdata/terraform_required_version/multiple-invalid/.expected/issues.json b/tflint-ruleset-blueprint/rules/testdata/terraform_required_version/multiple-invalid/.expected/issues.json new file mode 100644 index 000000000000..1cc9e956b00d --- /dev/null +++ b/tflint-ruleset-blueprint/rules/testdata/terraform_required_version/multiple-invalid/.expected/issues.json @@ -0,0 +1,142 @@ +[ + { + "Message": "required_version is not inclusive of the the minimum \"1.3\" and maximum \"1.5\" terraform required_version: \">= 1\"", + "Range": { + "Filename": "main.tf", + "Start": { + "Line": 1, + "Column": 1 + }, + "End": { + "Line": 1, + "Column": 10 + } + } + }, + { + "Message": "required_version is not inclusive of the the minimum \"1.3\" and maximum \"1.5\" terraform required_version: \">= 1.1\"", + "Range": { + "Filename": "main.tf", + "Start": { + "Line": 5, + "Column": 1 + }, + "End": { + "Line": 5, + "Column": 10 + } + } + }, + { + "Message": "required_version is not inclusive of the the minimum \"1.3\" and maximum \"1.5\" terraform required_version: \">= 1.1.0\"", + "Range": { + "Filename": "main.tf", + "Start": { + "Line": 9, + "Column": 1 + }, + "End": { + "Line": 9, + "Column": 10 + } + } + }, + { + "Message": "required_version is not inclusive of the the minimum \"1.3\" and maximum \"1.5\" terraform required_version: \">=1.1.0\"", + "Range": { + "Filename": "main.tf", + "Start": { + "Line": 13, + "Column": 1 + }, + "End": { + "Line": 13, + "Column": 10 + } + } + }, + { + "Message": "required_version is not inclusive of the the minimum \"1.3\" and maximum \"1.5\" terraform required_version: \">= 1.1.0, < 2.0\"", + "Range": { + "Filename": "main.tf", + "Start": { + "Line": 17, + "Column": 1 + }, + "End": { + "Line": 17, + "Column": 10 + } + } + }, + { + "Message": "required_version is not inclusive of the the minimum \"1.3\" and maximum \"1.5\" terraform required_version: \">=0.13.0\"", + "Range": { + "Filename": "main.tf", + "Start": { + "Line": 21, + "Column": 1 + }, + "End": { + "Line": 21, + "Column": 10 + } + } + }, + { + "Message": "required_version is not inclusive of the the minimum \"1.3\" and maximum \"1.5\" terraform required_version: \"=0.13.0\"", + "Range": { + "Filename": "main.tf", + "Start": { + "Line": 25, + "Column": 1 + }, + "End": { + "Line": 25, + "Column": 10 + } + } + }, + { + "Message": "required_version is not inclusive of the the minimum \"1.3\" and maximum \"1.5\" terraform required_version: \"0.13.0\"", + "Range": { + "Filename": "main.tf", + "Start": { + "Line": 29, + "Column": 1 + }, + "End": { + "Line": 29, + "Column": 10 + } + } + }, + { + "Message": "required_version is not inclusive of the the minimum \"1.3\" and maximum \"1.5\" terraform required_version: \">= 1.6.0\"", + "Range": { + "Filename": "main.tf", + "Start": { + "Line": 33, + "Column": 1 + }, + "End": { + "Line": 33, + "Column": 10 + } + } + }, + { + "Message": "required_version is not inclusive of the the minimum \"1.3\" and maximum \"1.5\" terraform required_version: \">= 1.6.0, < 2.0\"", + "Range": { + "Filename": "main.tf", + "Start": { + "Line": 37, + "Column": 1 + }, + "End": { + "Line": 37, + "Column": 10 + } + } + } +] diff --git a/tflint-ruleset-blueprint/rules/testdata/terraform_required_version/multiple-invalid/main.tf b/tflint-ruleset-blueprint/rules/testdata/terraform_required_version/multiple-invalid/main.tf new file mode 100644 index 000000000000..98c1c4c6a477 --- /dev/null +++ b/tflint-ruleset-blueprint/rules/testdata/terraform_required_version/multiple-invalid/main.tf @@ -0,0 +1,39 @@ +terraform { + required_version = ">= 1" +} + +terraform { + required_version = ">= 1.1" +} + +terraform { + required_version = ">= 1.1.0" +} + +terraform { + required_version = ">=1.1.0" +} + +terraform { + required_version = ">= 1.1.0, < 2.0" +} + +terraform { + required_version = ">=0.13.0" +} + +terraform { + required_version = "=0.13.0" +} + +terraform { + required_version = "0.13.0" +} + +terraform { + required_version = ">= 1.6.0" +} + +terraform { + required_version = ">= 1.6.0, < 2.0" +} diff --git a/tflint-ruleset-blueprint/rules/testdata/terraform_required_version/multiple-valid/.expected/issues.json b/tflint-ruleset-blueprint/rules/testdata/terraform_required_version/multiple-valid/.expected/issues.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/tflint-ruleset-blueprint/rules/testdata/terraform_required_version/multiple-valid/.expected/issues.json @@ -0,0 +1 @@ +[] diff --git a/tflint-ruleset-blueprint/rules/testdata/terraform_required_version/multiple-valid/main.tf b/tflint-ruleset-blueprint/rules/testdata/terraform_required_version/multiple-valid/main.tf new file mode 100644 index 000000000000..5d4e51b6c4b9 --- /dev/null +++ b/tflint-ruleset-blueprint/rules/testdata/terraform_required_version/multiple-valid/main.tf @@ -0,0 +1,39 @@ +terraform { + required_version = ">=1.3" +} + +terraform { + required_version = ">=1.3.0" +} + +terraform { + required_version = ">=v1.3" +} + +terraform { + required_version = ">=1.4" +} + +terraform { + required_version = ">= 1.3" +} + +terraform { + required_version = ">= v1.3" +} + +terraform { + required_version = ">=1.3, <2.0" +} + +terraform { + required_version = ">= 1.3, < 2.0" +} + +terraform { + required_version = "=1.3" +} + +terraform { + required_version = "1.3" +}