From f2426b432d46db4b04fe766a950415f2577e3981 Mon Sep 17 00:00:00 2001 From: neil-yechenwei Date: Fri, 12 Apr 2024 14:39:41 +0800 Subject: [PATCH 1/8] tfnfr25 --- .idea/.gitignore | 8 ++ .idea/modules.xml | 8 ++ .idea/tflint-ruleset-avm.iml | 9 ++ .idea/vcs.xml | 6 ++ requiredversion/required_version.go | 9 ++ requiredversion/required_version_test.go | 49 ++++++++++ requiredversion/requried_version.go | 108 +++++++++++++++++++++++ rules/rule_register.go | 4 +- 8 files changed, 199 insertions(+), 2 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/modules.xml create mode 100644 .idea/tflint-ruleset-avm.iml create mode 100644 .idea/vcs.xml create mode 100644 requiredversion/required_version.go create mode 100644 requiredversion/required_version_test.go create mode 100644 requiredversion/requried_version.go diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..b7201eb --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/tflint-ruleset-avm.iml b/.idea/tflint-ruleset-avm.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/tflint-ruleset-avm.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/requiredversion/required_version.go b/requiredversion/required_version.go new file mode 100644 index 0000000..374e06b --- /dev/null +++ b/requiredversion/required_version.go @@ -0,0 +1,9 @@ +// Package requiredversion provides the rules for the requiredversion category. +// Add the rules to the below slice to enable them. +package requiredversion + +import "github.com/terraform-linters/tflint-plugin-sdk/tflint" + +var Rules = []tflint.Rule{ + NewRequiredVersionRule("required_version_tfnfr25", "https://azure.github.io/Azure-Verified-Modules/specs/terraform/#id-tfnfr25---category-code-style---verified-modules-requirements"), +} diff --git a/requiredversion/required_version_test.go b/requiredversion/required_version_test.go new file mode 100644 index 0000000..fc359c2 --- /dev/null +++ b/requiredversion/required_version_test.go @@ -0,0 +1,49 @@ +package requiredversion_test + +import ( + "testing" + + "github.com/Azure/tflint-ruleset-avm/requiredversion" + "github.com/terraform-linters/tflint-plugin-sdk/helper" +) + +func TestRequiredVersion(t *testing.T) { + cases := []struct { + desc string + config string + requiredName string + issues helper.Issues + }{ + { + desc: "required_version = '~> #.#', ok", + config: `terraform { + required_version = "~> 0.12.29" + required_providers { + azurerm = { + version = ">= 3.98.0" + source = "hashicorp/azurerm" + } + } +}`, + requiredName: "required_version", + issues: helper.Issues{}, + }, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.desc, func(t *testing.T) { + t.Parallel() + rule := requiredversion.NewRequiredVersionRule("required_version", "") + filename := "variables.tf" + + runner := helper.TestRunner(t, map[string]string{filename: tc.config}) + + if err := rule.Check(runner); err != nil { + t.Fatalf("Unexpected error occurred: %s", err) + } + + helper.AssertIssuesWithoutRange(t, tc.issues, runner.Issues) + }) + } +} diff --git a/requiredversion/requried_version.go b/requiredversion/requried_version.go new file mode 100644 index 0000000..3404eb4 --- /dev/null +++ b/requiredversion/requried_version.go @@ -0,0 +1,108 @@ +package requiredversion + +import ( + "fmt" + + "github.com/hashicorp/hcl/v2" + "github.com/terraform-linters/tflint-plugin-sdk/hclext" + "github.com/terraform-linters/tflint-plugin-sdk/tflint" +) + +// variableBodySchema is the schema for the variable block that we want to extract from the config. +var requiredVersionBodySchema = &hclext.BodySchema{ + Blocks: []hclext.BlockSchema{ + { + Type: "terraform", + Body: &hclext.BodySchema{ + Attributes: []hclext.AttributeSchema{ + {Name: "required_version"}, + }, + Blocks: []hclext.BlockSchema{ + { + Type: "required_providers", + Body: &hclext.BodySchema{ + Attributes: []hclext.AttributeSchema{ + {Name: "azurerm"}, + }, + }, + }, + }, + }, + }, + }, +} + +// Check interface compliance with the tflint.Rule. +var _ tflint.Rule = new(RequiredVersionRule) + +// RequiredVersionRule is the struct that represents a rule that +// check for the correct usage of an interface. +type RequiredVersionRule struct { + tflint.DefaultRule + link string + ruleName string +} + +// NewRequiredVersionRule returns a new rule with the given variable. +func NewRequiredVersionRule(ruleName, link string) *RequiredVersionRule { + return &RequiredVersionRule{ + ruleName: ruleName, + link: link, + } +} + +// Name returns the rule name. +func (or *RequiredVersionRule) Name() string { + return or.ruleName +} + +// Link returns the link to the rule documentation. +func (or *RequiredVersionRule) Link() string { + return or.link +} + +// Enabled returns whether the rule is enabled. +func (or *RequiredVersionRule) Enabled() bool { + return true +} + +// Severity returns the severity of the rule. +func (or *RequiredVersionRule) Severity() tflint.Severity { + return tflint.ERROR +} + +// Check checks whether the module satisfies the interface. +// It will search for a variable with the same name as the interface. +// It will check the type, default value and nullable attributes. +func (vcr *RequiredVersionRule) Check(r tflint.Runner) error { + path, err := r.GetModulePath() + if err != nil { + return err + } + if !path.IsRoot() { + // This rule does not evaluate child modules. + return nil + } + + // Define the schema that we want to pull out of the module content. + body, err := r.GetModuleContent( + requiredVersionBodySchema, + &tflint.GetModuleContentOption{ExpandMode: tflint.ExpandModeNone}) + if err != nil { + return err + } + + // Iterate over the outputs and check for the name we are interested in. + for _, b := range body.Blocks { + if b.Labels[0] == "abc" { + return nil + } + } + return r.EmitIssue( + vcr, + fmt.Sprintf("module owners MUST output the `%s` in their modules", "aaa"), + hcl.Range{ + Filename: "outputs.tf", + }, + ) +} diff --git a/rules/rule_register.go b/rules/rule_register.go index 2e1690f..dd70fd3 100644 --- a/rules/rule_register.go +++ b/rules/rule_register.go @@ -5,7 +5,7 @@ import ( "github.com/Azure/tflint-ruleset-avm/interfaces" "github.com/Azure/tflint-ruleset-avm/outputs" - + "github.com/Azure/tflint-ruleset-avm/requiredversion" "github.com/Azure/tflint-ruleset-avm/waf" azurerm "github.com/Azure/tflint-ruleset-azurerm-ext/rules" basic "github.com/Azure/tflint-ruleset-basic-ext/rules" @@ -19,7 +19,6 @@ var Rules = func() []tflint.Rule { Wrap(basic.NewTerraformModuleProviderDeclarationRule()), Wrap(basic.NewTerraformOutputSeparateRule()), Wrap(basic.NewTerraformRequiredProvidersDeclarationRule()), - Wrap(basic.NewTerraformRequiredVersionDeclarationRule()), Wrap(basic.NewTerraformSensitiveVariableNoDefaultRule()), Wrap(basic.NewTerraformVariableNullableFalseRule()), Wrap(basic.NewTerraformVariableSeparateRule()), @@ -29,6 +28,7 @@ var Rules = func() []tflint.Rule { waf.Rules, interfaces.Rules, outputs.Rules, + requiredversion.Rules, ) }() From 194aa26101d54c41739b803863699ebcc7e8c2b1 Mon Sep 17 00:00:00 2001 From: neil-yechenwei Date: Fri, 12 Apr 2024 17:02:39 +0800 Subject: [PATCH 2/8] update code --- requiredversion/required_version_test.go | 174 +++++++++++++++++++++-- requiredversion/requried_version.go | 133 +++++++++-------- 2 files changed, 229 insertions(+), 78 deletions(-) diff --git a/requiredversion/required_version_test.go b/requiredversion/required_version_test.go index fc359c2..ba9e1f4 100644 --- a/requiredversion/required_version_test.go +++ b/requiredversion/required_version_test.go @@ -4,20 +4,97 @@ import ( "testing" "github.com/Azure/tflint-ruleset-avm/requiredversion" + "github.com/hashicorp/hcl/v2" "github.com/terraform-linters/tflint-plugin-sdk/helper" ) func TestRequiredVersion(t *testing.T) { cases := []struct { - desc string - config string - requiredName string - issues helper.Issues + desc string + files map[string]string + issues helper.Issues }{ { - desc: "required_version = '~> #.#', ok", - config: `terraform { - required_version = "~> 0.12.29" + desc: "required_version = '~> xx.xx.xx', ok", + files: map[string]string{ + "terraform.tf": `terraform { + required_version = "~> 0.12.29" + required_providers { + azurerm = { + version = ">= 3.98.0" + source = "hashicorp/azurerm" + } + } + }`, + }, + issues: helper.Issues{}, + }, + { + desc: "required_version = '>= xx.xx.xx, < xx.xx.xx', ok", + files: map[string]string{ + "terraform.tf": `terraform { + required_version = ">= 1.6.0, < 1.7.4" + required_providers { + azurerm = { + version = ">= 3.98.0" + source = "hashicorp/azurerm" + } + } + }`, + }, + issues: helper.Issues{}, + }, + { + desc: "required_version = 'xx.xx.xx', not ok", + files: map[string]string{ + "terraform.tf": `terraform { + required_version = "0.12.29" + required_providers { + azurerm = { + version = ">= 3.98.0" + source = "hashicorp/azurerm" + } + } +}`, + }, + issues: helper.Issues{ + { + Rule: requiredversion.NewRequiredVersionRule("required_version", ""), + Message: "The `required_version` property constraint can use the ~> #.# or the >= #.#.#, < #.#.# format", + Range: hcl.Range{ + Filename: "terraform.tf", + }, + }, + }, + }, + { + desc: "required_version = '>= xx.xx.xx', not ok", + files: map[string]string{ + "terraform.tf": `terraform { + required_version = ">= 0.12.29" + required_providers { + azurerm = { + version = ">= 3.98.0" + source = "hashicorp/azurerm" + } + } +}`, + }, + issues: helper.Issues{ + { + Rule: requiredversion.NewRequiredVersionRule("required_version", ""), + Message: "The `required_version` property constraint can use the ~> #.# or the >= #.#.#, < #.#.# format", + Range: hcl.Range{ + Filename: "terraform.tf", + }, + }, + }, + }, + { + desc: "required_version = '< xx.xx.xx', not ok", + files: map[string]string{ + "terraform.tf": `terraform { + required_version = "< 1.7.4" required_providers { azurerm = { version = ">= 3.98.0" @@ -25,8 +102,84 @@ func TestRequiredVersion(t *testing.T) { } } }`, - requiredName: "required_version", - issues: helper.Issues{}, + }, + issues: helper.Issues{ + { + Rule: requiredversion.NewRequiredVersionRule("required_version", ""), + Message: "The `required_version` property constraint can use the ~> #.# or the >= #.#.#, < #.#.# format", + Range: hcl.Range{ + Filename: "terraform.tf", + }, + }, + }, + }, + { + desc: "required_version = ', xx.xx.xx', not ok", + files: map[string]string{ + "terraform.tf": `terraform { + required_version = ", 1.7.4" + required_providers { + azurerm = { + version = ">= 3.98.0" + source = "hashicorp/azurerm" + } + } +}`, + }, + issues: helper.Issues{ + { + Rule: requiredversion.NewRequiredVersionRule("required_version", ""), + Message: "The `required_version` property constraint can use the ~> #.# or the >= #.#.#, < #.#.# format", + Range: hcl.Range{ + Filename: "terraform.tf", + }, + }, + }, + }, + { + desc: "no required_version set for terraform block", + files: map[string]string{ + "terraform.tf": `terraform { + required_providers { + azurerm = { + version = ">= 3.98.0" + source = "hashicorp/azurerm" + } + } +}`, + }, + issues: helper.Issues{ + { + Rule: requiredversion.NewRequiredVersionRule("required_version", ""), + Message: "The `required_version` field should be declared in the `terraform` block", + Range: hcl.Range{ + Filename: "terraform.tf", + }, + }, + }, + }, + { + desc: "required_version is not placed at the beginning of terraform block", + files: map[string]string{ + "terraform.tf": `terraform { + required_providers { + azurerm = { + version = ">= 3.98.0" + source = "hashicorp/azurerm" + } + } + required_version = "~> 1.7.4" + }`, + }, + issues: helper.Issues{ + { + Rule: requiredversion.NewRequiredVersionRule("required_version", ""), + Message: "The `required_version` field should be declared at the beginning of `terraform` block", + Range: hcl.Range{ + Filename: "terraform.tf", + }, + }, + }, }, } @@ -35,9 +188,8 @@ func TestRequiredVersion(t *testing.T) { t.Run(tc.desc, func(t *testing.T) { t.Parallel() rule := requiredversion.NewRequiredVersionRule("required_version", "") - filename := "variables.tf" - runner := helper.TestRunner(t, map[string]string{filename: tc.config}) + runner := helper.TestRunner(t, tc.files) if err := rule.Check(runner); err != nil { t.Fatalf("Unexpected error occurred: %s", err) diff --git a/requiredversion/requried_version.go b/requiredversion/requried_version.go index 3404eb4..1170828 100644 --- a/requiredversion/requried_version.go +++ b/requiredversion/requried_version.go @@ -1,49 +1,20 @@ package requiredversion import ( - "fmt" - "github.com/hashicorp/hcl/v2" - "github.com/terraform-linters/tflint-plugin-sdk/hclext" + "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/terraform-linters/tflint-plugin-sdk/tflint" + "strings" ) -// variableBodySchema is the schema for the variable block that we want to extract from the config. -var requiredVersionBodySchema = &hclext.BodySchema{ - Blocks: []hclext.BlockSchema{ - { - Type: "terraform", - Body: &hclext.BodySchema{ - Attributes: []hclext.AttributeSchema{ - {Name: "required_version"}, - }, - Blocks: []hclext.BlockSchema{ - { - Type: "required_providers", - Body: &hclext.BodySchema{ - Attributes: []hclext.AttributeSchema{ - {Name: "azurerm"}, - }, - }, - }, - }, - }, - }, - }, -} - -// Check interface compliance with the tflint.Rule. var _ tflint.Rule = new(RequiredVersionRule) -// RequiredVersionRule is the struct that represents a rule that -// check for the correct usage of an interface. type RequiredVersionRule struct { tflint.DefaultRule - link string ruleName string + link string } -// NewRequiredVersionRule returns a new rule with the given variable. func NewRequiredVersionRule(ruleName, link string) *RequiredVersionRule { return &RequiredVersionRule{ ruleName: ruleName, @@ -51,58 +22,86 @@ func NewRequiredVersionRule(ruleName, link string) *RequiredVersionRule { } } -// Name returns the rule name. -func (or *RequiredVersionRule) Name() string { - return or.ruleName +func (t *RequiredVersionRule) Name() string { + return t.ruleName } -// Link returns the link to the rule documentation. -func (or *RequiredVersionRule) Link() string { - return or.link +func (t *RequiredVersionRule) Link() string { + return t.link } -// Enabled returns whether the rule is enabled. -func (or *RequiredVersionRule) Enabled() bool { +func (t *RequiredVersionRule) Enabled() bool { return true } -// Severity returns the severity of the rule. -func (or *RequiredVersionRule) Severity() tflint.Severity { +func (t *RequiredVersionRule) Severity() tflint.Severity { return tflint.ERROR } -// Check checks whether the module satisfies the interface. -// It will search for a variable with the same name as the interface. -// It will check the type, default value and nullable attributes. -func (vcr *RequiredVersionRule) Check(r tflint.Runner) error { - path, err := r.GetModulePath() +func (t *RequiredVersionRule) Check(r tflint.Runner) error { + tFile, err := r.GetFile("terraform.tf") if err != nil { return err } - if !path.IsRoot() { - // This rule does not evaluate child modules. + + body, ok := tFile.Body.(*hclsyntax.Body) + if !ok { return nil } - // Define the schema that we want to pull out of the module content. - body, err := r.GetModuleContent( - requiredVersionBodySchema, - &tflint.GetModuleContentOption{ExpandMode: tflint.ExpandModeNone}) - if err != nil { - return err - } + for _, terraformBlock := range body.Blocks { + if terraformBlock.Type == "terraform" { + + if requiredVersion, exists := terraformBlock.Body.Attributes["required_version"]; exists { + err := r.EvaluateExpr(requiredVersion.Expr, func(requiredVersionVal string) error { + if !strings.Contains(requiredVersionVal, "~>") && !(strings.Contains(requiredVersionVal, ">=") && strings.Contains(requiredVersionVal, ",") && strings.Contains(requiredVersionVal, "<")) { + return r.EmitIssue( + t, + "The `required_version` property constraint can use the ~> #.# or the >= #.#.#, < #.#.# format", + requiredVersion.NameRange, + ) + } - // Iterate over the outputs and check for the name we are interested in. - for _, b := range body.Blocks { - if b.Labels[0] == "abc" { - return nil + comparePos := func(pos1 hcl.Pos, pos2 hcl.Pos) bool { + if pos1.Line != pos2.Line { + return pos1.Line < pos2.Line + } + return pos1.Column < pos2.Column + } + + for _, attr := range terraformBlock.Body.Attributes { + if attr.Name != "required_version" && comparePos(attr.Range().Start, requiredVersion.Range().Start) { + return r.EmitIssue( + t, + "The `required_version` field should be declared at the beginning of `terraform` block", + requiredVersion.NameRange, + ) + } + } + + for _, block := range terraformBlock.Body.Blocks { + if comparePos(block.Range().Start, requiredVersion.Range().Start) { + return r.EmitIssue( + t, + "The `required_version` field should be declared at the beginning of `terraform` block", + requiredVersion.NameRange, + ) + } + } + return nil + }, &tflint.EvaluateExprOption{ModuleCtx: tflint.RootModuleCtxType}) + if err != nil { + return err + } + } else { + return r.EmitIssue( + t, + "The `required_version` field should be declared in the `terraform` block", + terraformBlock.DefRange(), + ) + } } } - return r.EmitIssue( - vcr, - fmt.Sprintf("module owners MUST output the `%s` in their modules", "aaa"), - hcl.Range{ - Filename: "outputs.tf", - }, - ) + + return nil } From f2491ea4a38cd19b85c943df7236b80f9ecdc38e Mon Sep 17 00:00:00 2001 From: neil-yechenwei Date: Fri, 12 Apr 2024 17:06:02 +0800 Subject: [PATCH 3/8] update code --- .idea/.gitignore | 8 --- .idea/modules.xml | 8 --- .idea/tflint-ruleset-avm.iml | 9 --- .idea/vcs.xml | 6 -- .idea/workspace.xml | 108 +++++++++++++++++++++++++++++++++++ 5 files changed, 108 insertions(+), 31 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/modules.xml delete mode 100644 .idea/tflint-ruleset-avm.iml delete mode 100644 .idea/vcs.xml create mode 100644 .idea/workspace.xml diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b8..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index b7201eb..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/tflint-ruleset-avm.iml b/.idea/tflint-ruleset-avm.iml deleted file mode 100644 index 5e764c4..0000000 --- a/.idea/tflint-ruleset-avm.iml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..acef1f3 --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + + + + file://$PROJECT_DIR$/outputs/required_output_test.go + 67 + + + file://$PROJECT_DIR$/requiredversion/requried_version.go + 56 + + + + + \ No newline at end of file From 8a74b0855ed9d8b96947ca44b64b0525d3a7cec8 Mon Sep 17 00:00:00 2001 From: neil-yechenwei Date: Fri, 12 Apr 2024 17:06:29 +0800 Subject: [PATCH 4/8] update code --- .idea/workspace.xml | 108 -------------------------------------------- 1 file changed, 108 deletions(-) delete mode 100644 .idea/workspace.xml diff --git a/.idea/workspace.xml b/.idea/workspace.xml deleted file mode 100644 index acef1f3..0000000 --- a/.idea/workspace.xml +++ /dev/null @@ -1,108 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - true - - - - - - file://$PROJECT_DIR$/outputs/required_output_test.go - 67 - - - file://$PROJECT_DIR$/requiredversion/requried_version.go - 56 - - - - - \ No newline at end of file From e347e1df2dc3df021cd729252d8ff6a3418ee860 Mon Sep 17 00:00:00 2001 From: neil-yechenwei Date: Fri, 12 Apr 2024 17:15:52 +0800 Subject: [PATCH 5/8] update code --- integration/optional-defaults-incorrect/terraform.tf | 9 +++++++++ integration/optional-defaults/terraform.tf | 9 +++++++++ integration/simplevaluerule-null-value/terraform.tf | 9 +++++++++ integration/unknownrule-null-incorrect/terraform.tf | 9 +++++++++ integration/unknownrule-null/terraform.tf | 9 +++++++++ 5 files changed, 45 insertions(+) create mode 100644 integration/optional-defaults-incorrect/terraform.tf create mode 100644 integration/optional-defaults/terraform.tf create mode 100644 integration/simplevaluerule-null-value/terraform.tf create mode 100644 integration/unknownrule-null-incorrect/terraform.tf create mode 100644 integration/unknownrule-null/terraform.tf diff --git a/integration/optional-defaults-incorrect/terraform.tf b/integration/optional-defaults-incorrect/terraform.tf new file mode 100644 index 0000000..70c5967 --- /dev/null +++ b/integration/optional-defaults-incorrect/terraform.tf @@ -0,0 +1,9 @@ +terraform { + required_version = "~> 1.7.0" + required_providers { + azurerm = { + version = ">= 3.98.0" + source = "hashicorp/azurerm" + } + } +} \ No newline at end of file diff --git a/integration/optional-defaults/terraform.tf b/integration/optional-defaults/terraform.tf new file mode 100644 index 0000000..70c5967 --- /dev/null +++ b/integration/optional-defaults/terraform.tf @@ -0,0 +1,9 @@ +terraform { + required_version = "~> 1.7.0" + required_providers { + azurerm = { + version = ">= 3.98.0" + source = "hashicorp/azurerm" + } + } +} \ No newline at end of file diff --git a/integration/simplevaluerule-null-value/terraform.tf b/integration/simplevaluerule-null-value/terraform.tf new file mode 100644 index 0000000..70c5967 --- /dev/null +++ b/integration/simplevaluerule-null-value/terraform.tf @@ -0,0 +1,9 @@ +terraform { + required_version = "~> 1.7.0" + required_providers { + azurerm = { + version = ">= 3.98.0" + source = "hashicorp/azurerm" + } + } +} \ No newline at end of file diff --git a/integration/unknownrule-null-incorrect/terraform.tf b/integration/unknownrule-null-incorrect/terraform.tf new file mode 100644 index 0000000..70c5967 --- /dev/null +++ b/integration/unknownrule-null-incorrect/terraform.tf @@ -0,0 +1,9 @@ +terraform { + required_version = "~> 1.7.0" + required_providers { + azurerm = { + version = ">= 3.98.0" + source = "hashicorp/azurerm" + } + } +} \ No newline at end of file diff --git a/integration/unknownrule-null/terraform.tf b/integration/unknownrule-null/terraform.tf new file mode 100644 index 0000000..70c5967 --- /dev/null +++ b/integration/unknownrule-null/terraform.tf @@ -0,0 +1,9 @@ +terraform { + required_version = "~> 1.7.0" + required_providers { + azurerm = { + version = ">= 3.98.0" + source = "hashicorp/azurerm" + } + } +} \ No newline at end of file From cb7c2e19ae21f3c45f159c3cc3e4a1d14ca58b85 Mon Sep 17 00:00:00 2001 From: neil-yechenwei Date: Fri, 12 Apr 2024 17:19:26 +0800 Subject: [PATCH 6/8] update code --- integration/optional-defaults-incorrect/terraform.tf | 2 +- integration/optional-defaults/terraform.tf | 2 +- integration/simplevaluerule-null-value/terraform.tf | 2 +- integration/unknownrule-null-incorrect/terraform.tf | 2 +- integration/unknownrule-null/terraform.tf | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/integration/optional-defaults-incorrect/terraform.tf b/integration/optional-defaults-incorrect/terraform.tf index 70c5967..bb2aa09 100644 --- a/integration/optional-defaults-incorrect/terraform.tf +++ b/integration/optional-defaults-incorrect/terraform.tf @@ -6,4 +6,4 @@ terraform { source = "hashicorp/azurerm" } } -} \ No newline at end of file +} diff --git a/integration/optional-defaults/terraform.tf b/integration/optional-defaults/terraform.tf index 70c5967..bb2aa09 100644 --- a/integration/optional-defaults/terraform.tf +++ b/integration/optional-defaults/terraform.tf @@ -6,4 +6,4 @@ terraform { source = "hashicorp/azurerm" } } -} \ No newline at end of file +} diff --git a/integration/simplevaluerule-null-value/terraform.tf b/integration/simplevaluerule-null-value/terraform.tf index 70c5967..bb2aa09 100644 --- a/integration/simplevaluerule-null-value/terraform.tf +++ b/integration/simplevaluerule-null-value/terraform.tf @@ -6,4 +6,4 @@ terraform { source = "hashicorp/azurerm" } } -} \ No newline at end of file +} diff --git a/integration/unknownrule-null-incorrect/terraform.tf b/integration/unknownrule-null-incorrect/terraform.tf index 70c5967..bb2aa09 100644 --- a/integration/unknownrule-null-incorrect/terraform.tf +++ b/integration/unknownrule-null-incorrect/terraform.tf @@ -6,4 +6,4 @@ terraform { source = "hashicorp/azurerm" } } -} \ No newline at end of file +} diff --git a/integration/unknownrule-null/terraform.tf b/integration/unknownrule-null/terraform.tf index 70c5967..bb2aa09 100644 --- a/integration/unknownrule-null/terraform.tf +++ b/integration/unknownrule-null/terraform.tf @@ -6,4 +6,4 @@ terraform { source = "hashicorp/azurerm" } } -} \ No newline at end of file +} From c2278b7c847e49c21d1a2e5c0553d4205c984235 Mon Sep 17 00:00:00 2001 From: neil-yechenwei Date: Mon, 15 Apr 2024 09:07:43 +0800 Subject: [PATCH 7/8] update code --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 71bb79c..7d379e8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ # Ignore binary tflint-ruleset-avm tflint-ruleset-avm.exe +.idea From eebaa76febcf724ea272faaac54a3d53d1062d63 Mon Sep 17 00:00:00 2001 From: hezijie Date: Mon, 15 Apr 2024 14:32:40 +0800 Subject: [PATCH 8/8] refactor code --- requiredversion/required_version.go | 9 -- requiredversion/requried_version.go | 107 ---------------- rules/required_version.go | 116 ++++++++++++++++++ .../required_version_test.go | 18 +-- rules/rule_register.go | 3 +- 5 files changed, 126 insertions(+), 127 deletions(-) delete mode 100644 requiredversion/required_version.go delete mode 100644 requiredversion/requried_version.go create mode 100644 rules/required_version.go rename {requiredversion => rules}/required_version_test.go (86%) diff --git a/requiredversion/required_version.go b/requiredversion/required_version.go deleted file mode 100644 index 374e06b..0000000 --- a/requiredversion/required_version.go +++ /dev/null @@ -1,9 +0,0 @@ -// Package requiredversion provides the rules for the requiredversion category. -// Add the rules to the below slice to enable them. -package requiredversion - -import "github.com/terraform-linters/tflint-plugin-sdk/tflint" - -var Rules = []tflint.Rule{ - NewRequiredVersionRule("required_version_tfnfr25", "https://azure.github.io/Azure-Verified-Modules/specs/terraform/#id-tfnfr25---category-code-style---verified-modules-requirements"), -} diff --git a/requiredversion/requried_version.go b/requiredversion/requried_version.go deleted file mode 100644 index 1170828..0000000 --- a/requiredversion/requried_version.go +++ /dev/null @@ -1,107 +0,0 @@ -package requiredversion - -import ( - "github.com/hashicorp/hcl/v2" - "github.com/hashicorp/hcl/v2/hclsyntax" - "github.com/terraform-linters/tflint-plugin-sdk/tflint" - "strings" -) - -var _ tflint.Rule = new(RequiredVersionRule) - -type RequiredVersionRule struct { - tflint.DefaultRule - ruleName string - link string -} - -func NewRequiredVersionRule(ruleName, link string) *RequiredVersionRule { - return &RequiredVersionRule{ - ruleName: ruleName, - link: link, - } -} - -func (t *RequiredVersionRule) Name() string { - return t.ruleName -} - -func (t *RequiredVersionRule) Link() string { - return t.link -} - -func (t *RequiredVersionRule) Enabled() bool { - return true -} - -func (t *RequiredVersionRule) Severity() tflint.Severity { - return tflint.ERROR -} - -func (t *RequiredVersionRule) Check(r tflint.Runner) error { - tFile, err := r.GetFile("terraform.tf") - if err != nil { - return err - } - - body, ok := tFile.Body.(*hclsyntax.Body) - if !ok { - return nil - } - - for _, terraformBlock := range body.Blocks { - if terraformBlock.Type == "terraform" { - - if requiredVersion, exists := terraformBlock.Body.Attributes["required_version"]; exists { - err := r.EvaluateExpr(requiredVersion.Expr, func(requiredVersionVal string) error { - if !strings.Contains(requiredVersionVal, "~>") && !(strings.Contains(requiredVersionVal, ">=") && strings.Contains(requiredVersionVal, ",") && strings.Contains(requiredVersionVal, "<")) { - return r.EmitIssue( - t, - "The `required_version` property constraint can use the ~> #.# or the >= #.#.#, < #.#.# format", - requiredVersion.NameRange, - ) - } - - comparePos := func(pos1 hcl.Pos, pos2 hcl.Pos) bool { - if pos1.Line != pos2.Line { - return pos1.Line < pos2.Line - } - return pos1.Column < pos2.Column - } - - for _, attr := range terraformBlock.Body.Attributes { - if attr.Name != "required_version" && comparePos(attr.Range().Start, requiredVersion.Range().Start) { - return r.EmitIssue( - t, - "The `required_version` field should be declared at the beginning of `terraform` block", - requiredVersion.NameRange, - ) - } - } - - for _, block := range terraformBlock.Body.Blocks { - if comparePos(block.Range().Start, requiredVersion.Range().Start) { - return r.EmitIssue( - t, - "The `required_version` field should be declared at the beginning of `terraform` block", - requiredVersion.NameRange, - ) - } - } - return nil - }, &tflint.EvaluateExprOption{ModuleCtx: tflint.RootModuleCtxType}) - if err != nil { - return err - } - } else { - return r.EmitIssue( - t, - "The `required_version` field should be declared in the `terraform` block", - terraformBlock.DefRange(), - ) - } - } - } - - return nil -} diff --git a/rules/required_version.go b/rules/required_version.go new file mode 100644 index 0000000..8586b1d --- /dev/null +++ b/rules/required_version.go @@ -0,0 +1,116 @@ +package rules + +import ( + "strings" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/terraform-linters/tflint-plugin-sdk/tflint" +) + +var _ tflint.Rule = new(RequiredVersionRule) + +type RequiredVersionRule struct { + tflint.DefaultRule +} + +func NewRequiredVersionRule() *RequiredVersionRule { + return &RequiredVersionRule{} +} + +func (t *RequiredVersionRule) Name() string { + return "required_version_tfnfr25" +} + +func (t *RequiredVersionRule) Link() string { + return "https://azure.github.io/Azure-Verified-Modules/specs/terraform/#id-tfnfr25---category-code-style---verified-modules-requirements" +} + +func (t *RequiredVersionRule) Enabled() bool { + return true +} + +func (t *RequiredVersionRule) Severity() tflint.Severity { + return tflint.ERROR +} + +func (t *RequiredVersionRule) Check(r tflint.Runner) error { + tFile, err := r.GetFile("terraform.tf") + if err != nil { + return err + } + + body, ok := tFile.Body.(*hclsyntax.Body) + if !ok { + return nil + } + + for _, terraformBlock := range body.Blocks { + if terraformBlock.Type != "terraform" { + continue + } + + requiredVersion, exists := terraformBlock.Body.Attributes["required_version"] + if !exists { + return r.EmitIssue( + t, + "The `required_version` field should be declared in the `terraform` block", + terraformBlock.DefRange(), + ) + } + if err = r.EvaluateExpr(requiredVersion.Expr, t.versionHasLimitedMajorVersion(r, requiredVersion.NameRange), + &tflint.EvaluateExprOption{ModuleCtx: tflint.RootModuleCtxType}); err != nil { + return err + } + + if !t.requiredVersionIsOnTheTop(terraformBlock) { + return r.EmitIssue( + t, + "The `required_version` field should be declared at the beginning of `terraform` block", + requiredVersion.NameRange, + ) + } + } + + return nil +} + +func (t *RequiredVersionRule) requiredVersionIsOnTheTop(terraformBlock *hclsyntax.Block) bool { + requiredVersion := terraformBlock.Body.Attributes["required_version"] + comparePos := func(pos1 hcl.Pos, pos2 hcl.Pos) bool { + if pos1.Line != pos2.Line { + return pos1.Line < pos2.Line + } + return pos1.Column < pos2.Column + } + + for _, attr := range terraformBlock.Body.Attributes { + if attr.Name == "required_version" { + continue + } + if comparePos(attr.Range().Start, requiredVersion.Range().Start) { + return false + } + } + + for _, block := range terraformBlock.Body.Blocks { + if comparePos(block.Range().Start, requiredVersion.Range().Start) { + return false + } + } + return true +} + +func (t *RequiredVersionRule) versionHasLimitedMajorVersion(r tflint.Runner, issueRange hcl.Range) func(string) error { + return func(requiredVersionVal string) error { + if !strings.Contains(requiredVersionVal, "~>") && !(strings.Contains(requiredVersionVal, ">") && strings.Contains(requiredVersionVal, "<")) { + return r.EmitIssue( + t, + "The `required_version` property constraint can use the ~> #.# or the >= #.#.#, < #.#.# format", + issueRange, + ) + } + + return nil + } +} diff --git a/requiredversion/required_version_test.go b/rules/required_version_test.go similarity index 86% rename from requiredversion/required_version_test.go rename to rules/required_version_test.go index ba9e1f4..04dfb02 100644 --- a/requiredversion/required_version_test.go +++ b/rules/required_version_test.go @@ -1,9 +1,9 @@ -package requiredversion_test +package rules_test import ( + "github.com/Azure/tflint-ruleset-avm/rules" "testing" - "github.com/Azure/tflint-ruleset-avm/requiredversion" "github.com/hashicorp/hcl/v2" "github.com/terraform-linters/tflint-plugin-sdk/helper" ) @@ -59,7 +59,7 @@ func TestRequiredVersion(t *testing.T) { }, issues: helper.Issues{ { - Rule: requiredversion.NewRequiredVersionRule("required_version", ""), + Rule: rules.NewRequiredVersionRule(), Message: "The `required_version` property constraint can use the ~> #.# or the >= #.#.#, < #.#.# format", Range: hcl.Range{ Filename: "terraform.tf", @@ -82,7 +82,7 @@ func TestRequiredVersion(t *testing.T) { }, issues: helper.Issues{ { - Rule: requiredversion.NewRequiredVersionRule("required_version", ""), + Rule: rules.NewRequiredVersionRule(), Message: "The `required_version` property constraint can use the ~> #.# or the >= #.#.#, < #.#.# format", Range: hcl.Range{ Filename: "terraform.tf", @@ -105,7 +105,7 @@ func TestRequiredVersion(t *testing.T) { }, issues: helper.Issues{ { - Rule: requiredversion.NewRequiredVersionRule("required_version", ""), + Rule: rules.NewRequiredVersionRule(), Message: "The `required_version` property constraint can use the ~> #.# or the >= #.#.#, < #.#.# format", Range: hcl.Range{ Filename: "terraform.tf", @@ -128,7 +128,7 @@ func TestRequiredVersion(t *testing.T) { }, issues: helper.Issues{ { - Rule: requiredversion.NewRequiredVersionRule("required_version", ""), + Rule: rules.NewRequiredVersionRule(), Message: "The `required_version` property constraint can use the ~> #.# or the >= #.#.#, < #.#.# format", Range: hcl.Range{ Filename: "terraform.tf", @@ -150,7 +150,7 @@ func TestRequiredVersion(t *testing.T) { }, issues: helper.Issues{ { - Rule: requiredversion.NewRequiredVersionRule("required_version", ""), + Rule: rules.NewRequiredVersionRule(), Message: "The `required_version` field should be declared in the `terraform` block", Range: hcl.Range{ Filename: "terraform.tf", @@ -173,7 +173,7 @@ func TestRequiredVersion(t *testing.T) { }, issues: helper.Issues{ { - Rule: requiredversion.NewRequiredVersionRule("required_version", ""), + Rule: rules.NewRequiredVersionRule(), Message: "The `required_version` field should be declared at the beginning of `terraform` block", Range: hcl.Range{ Filename: "terraform.tf", @@ -187,7 +187,7 @@ func TestRequiredVersion(t *testing.T) { tc := tc t.Run(tc.desc, func(t *testing.T) { t.Parallel() - rule := requiredversion.NewRequiredVersionRule("required_version", "") + rule := rules.NewRequiredVersionRule() runner := helper.TestRunner(t, tc.files) diff --git a/rules/rule_register.go b/rules/rule_register.go index dd70fd3..9ae4c38 100644 --- a/rules/rule_register.go +++ b/rules/rule_register.go @@ -5,7 +5,6 @@ import ( "github.com/Azure/tflint-ruleset-avm/interfaces" "github.com/Azure/tflint-ruleset-avm/outputs" - "github.com/Azure/tflint-ruleset-avm/requiredversion" "github.com/Azure/tflint-ruleset-avm/waf" azurerm "github.com/Azure/tflint-ruleset-azurerm-ext/rules" basic "github.com/Azure/tflint-ruleset-basic-ext/rules" @@ -24,11 +23,11 @@ var Rules = func() []tflint.Rule { Wrap(basic.NewTerraformVariableSeparateRule()), Wrap(azurerm.NewAzurermResourceTagRule()), NewTerraformDotTfRule(), + NewRequiredVersionRule(), }, waf.Rules, interfaces.Rules, outputs.Rules, - requiredversion.Rules, ) }()