-
Notifications
You must be signed in to change notification settings - Fork 7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
TFNFR25 - Category: Code Style - Terraform block version requirements #22
base: main
Are you sure you want to change the base?
Changes from all commits
f2426b4
194aa26
f2491ea
8a74b08
e347e1d
cb7c2e1
c2278b7
eebaa76
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
# Ignore binary | ||
tflint-ruleset-avm | ||
tflint-ruleset-avm.exe | ||
.idea |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
terraform { | ||
required_version = "~> 1.7.0" | ||
required_providers { | ||
azurerm = { | ||
version = ">= 3.98.0" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please change the order. I assume the order should be like below this. Please also update others with same problem. azurerm = { |
||
source = "hashicorp/azurerm" | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
terraform { | ||
required_version = "~> 1.7.0" | ||
required_providers { | ||
azurerm = { | ||
version = ">= 3.98.0" | ||
source = "hashicorp/azurerm" | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
terraform { | ||
required_version = "~> 1.7.0" | ||
required_providers { | ||
azurerm = { | ||
version = ">= 3.98.0" | ||
source = "hashicorp/azurerm" | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
terraform { | ||
required_version = "~> 1.7.0" | ||
required_providers { | ||
azurerm = { | ||
version = ">= 3.98.0" | ||
source = "hashicorp/azurerm" | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
terraform { | ||
required_version = "~> 1.7.0" | ||
required_providers { | ||
azurerm = { | ||
version = ">= 3.98.0" | ||
source = "hashicorp/azurerm" | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,201 @@ | ||
package rules_test | ||
|
||
import ( | ||
"github.com/Azure/tflint-ruleset-avm/rules" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please sort the order. Thanks. |
||
"testing" | ||
|
||
"github.com/hashicorp/hcl/v2" | ||
"github.com/terraform-linters/tflint-plugin-sdk/helper" | ||
) | ||
|
||
func TestRequiredVersion(t *testing.T) { | ||
cases := []struct { | ||
desc string | ||
files map[string]string | ||
issues helper.Issues | ||
}{ | ||
{ | ||
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: rules.NewRequiredVersionRule(), | ||
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: rules.NewRequiredVersionRule(), | ||
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: rules.NewRequiredVersionRule(), | ||
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: rules.NewRequiredVersionRule(), | ||
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: rules.NewRequiredVersionRule(), | ||
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: rules.NewRequiredVersionRule(), | ||
Message: "The `required_version` field should be declared at the beginning of `terraform` block", | ||
Range: hcl.Range{ | ||
Filename: "terraform.tf", | ||
}, | ||
}, | ||
}, | ||
}, | ||
} | ||
|
||
for _, tc := range cases { | ||
tc := tc | ||
t.Run(tc.desc, func(t *testing.T) { | ||
t.Parallel() | ||
rule := rules.NewRequiredVersionRule() | ||
|
||
runner := helper.TestRunner(t, tc.files) | ||
|
||
if err := rule.Check(runner); err != nil { | ||
t.Fatalf("Unexpected error occurred: %s", err) | ||
} | ||
|
||
helper.AssertIssuesWithoutRange(t, tc.issues, runner.Issues) | ||
}) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume that version should be like "~> #.#" or ">= #.#, < #.#". Please also update others with same problem.